|
12 | 12 | """ |
13 | 13 |
|
14 | 14 | # Standard |
| 15 | +import atexit |
| 16 | +import multiprocessing |
| 17 | +import os |
| 18 | +import time |
15 | 19 | from typing import List, Optional |
16 | 20 |
|
17 | 21 | # Third-Party |
| 22 | +import requests |
18 | 23 | import typer |
19 | 24 |
|
20 | 25 | # First-Party |
21 | | -from mcpgateway.translate import main as translate_main |
| 26 | +from cforge.common import get_console, make_authenticated_request |
22 | 27 |
|
23 | 28 |
|
24 | 29 | def run( |
@@ -55,24 +60,45 @@ def run( |
55 | 60 | ), |
56 | 61 | stateless: bool = typer.Option(False, "--stateless", help="Use stateless mode for streamable HTTP (default: False)"), |
57 | 62 | json_response: bool = typer.Option(False, "--json-response", help="Return JSON responses instead of SSE streams for streamable HTTP (default: False)"), |
| 63 | + register: bool = typer.Option(True, "--register/--no-register", help="Auto-register the server with the configured Context Forge gateway (default: True)"), |
| 64 | + register_timeout: float = typer.Option(10.0, "--register-timeout", help="Timeout for registration health check (default 10s)"), |
| 65 | + temporary: bool = typer.Option(False, "--temporary", help="Unregister the server on exit (only applies if --register is enabled)"), |
| 66 | + server_name: Optional[str] = typer.Option(None, "--server-name", help="Name for the registered server (auto-generated if not provided)"), |
| 67 | + server_description: Optional[str] = typer.Option(None, "--server-description", help="Description for the registered server"), |
58 | 68 | ) -> None: |
59 | 69 | """Run MCP servers locally and expose them via SSE or streamable HTTP. |
60 | 70 |
|
61 | 71 | This command bridges between different MCP transport protocols: stdio/JSON-RPC, |
62 | 72 | HTTP/SSE, and streamable HTTP. It enables exposing local MCP servers over HTTP |
63 | 73 | or consuming remote endpoints as local stdio servers. |
64 | 74 |
|
| 75 | + By default, the server is automatically registered with the configured Context Forge |
| 76 | + gateway. Use --no-register to disable this behavior, or --temporary to automatically |
| 77 | + unregister the server when it exits. |
| 78 | +
|
65 | 79 | Examples: |
66 | 80 |
|
67 | | - # Expose a local MCP server via SSE |
| 81 | + # Expose a local MCP server via SSE (auto-registered) |
68 | 82 | cforge run --stdio "uvx mcp-server-git" --port 9000 |
69 | 83 |
|
| 84 | + # Expose without registering with the gateway |
| 85 | + cforge run --stdio "uvx mcp-server-git" --port 9000 --no-register |
| 86 | +
|
| 87 | + # Expose and auto-cleanup on exit |
| 88 | + cforge run --stdio "uvx mcp-server-git" --port 9000 --temporary |
| 89 | +
|
70 | 90 | # Expose via both SSE and streamable HTTP |
71 | 91 | cforge run --stdio "uvx mcp-server-git" --expose-sse --expose-streamable-http --port 9000 |
72 | | -
|
73 | | - # Expose via streamable HTTP with stateless mode |
74 | | - cforge run --stdio "uvx mcp-server-git" --expose-streamable-http --stateless --port 9000 |
75 | 92 | """ |
| 93 | + console = get_console() |
| 94 | + |
| 95 | + # Handle registration if enabled |
| 96 | + if register and not temporary: |
| 97 | + # Validate that we have something to register |
| 98 | + if not stdio and not grpc: |
| 99 | + console.print("[yellow]Warning: --register requires either --stdio or --grpc to be specified[/yellow]") |
| 100 | + register = False |
| 101 | + |
76 | 102 | # Build argument list for translate_main |
77 | 103 | args = [] |
78 | 104 |
|
@@ -135,5 +161,89 @@ def run( |
135 | 161 | if json_response: |
136 | 162 | args.append("--jsonResponse") |
137 | 163 |
|
138 | | - # Call the translate main function with constructed arguments |
139 | | - translate_main(args) |
| 164 | + # Import top-level translate here to avoid undesirable initialization |
| 165 | + # Third Party |
| 166 | + from mcpgateway.translate import main as translate_main |
| 167 | + |
| 168 | + # Launch the translation wrapper in a subprocess |
| 169 | + proc = multiprocessing.Process(target=translate_main, args=(args,)) |
| 170 | + proc.start() |
| 171 | + |
| 172 | + # Register if requested |
| 173 | + if register: |
| 174 | + |
| 175 | + # Default to SSE if no protocol specified |
| 176 | + is_sse = expose_sse or expose_streamable_http or (not expose_sse and not expose_streamable_http) |
| 177 | + |
| 178 | + registered_server_id: Optional[str] = None |
| 179 | + try: |
| 180 | + # Wait for the server to come up |
| 181 | + server_url_base = f"http://{host}:{port}" |
| 182 | + start_time = time.time() |
| 183 | + while time.time() - start_time <= register_timeout: |
| 184 | + try: |
| 185 | + res = requests.get(f"{server_url_base}/healthz", timeout=0.1) |
| 186 | + if res.status_code == 200: |
| 187 | + break |
| 188 | + except requests.exceptions.ConnectionError: |
| 189 | + time.sleep(0.5) |
| 190 | + |
| 191 | + # Build the server URL based on the protocol |
| 192 | + server_url = f"{server_url_base}{sse_path}" if is_sse else f"{server_url_base}/mcp" |
| 193 | + |
| 194 | + # Generate a name if not provided |
| 195 | + if server_name is None: |
| 196 | + if stdio: |
| 197 | + # Extract command name from stdio |
| 198 | + cmd_parts = stdio.split() |
| 199 | + cmd_name = "stdio-server" |
| 200 | + for part in cmd_parts: |
| 201 | + part = os.path.basename(part) |
| 202 | + # Skip known runners, flags, and env vars |
| 203 | + if part.replace("-", "").replace("_", "").isalnum() and not (part.startswith("-") or part in ["docker", "uvx", "npx", "python", "node", "run"] or "=" in part): |
| 204 | + cmd_name = part |
| 205 | + break |
| 206 | + server_name = f"{cmd_name}-{port}" |
| 207 | + elif grpc: |
| 208 | + server_name = f"grpc-{grpc.replace(':', '-')}" |
| 209 | + else: |
| 210 | + server_name = f"server-{port}" |
| 211 | + |
| 212 | + # Build registration payload |
| 213 | + registration_data = { |
| 214 | + "name": server_name, |
| 215 | + "url": server_url, |
| 216 | + "transport": "SSE" if is_sse else "STREAMABLEHTTP", |
| 217 | + } |
| 218 | + |
| 219 | + if server_description: |
| 220 | + registration_data["description"] = server_description |
| 221 | + |
| 222 | + # Register the server |
| 223 | + console.print(f"[cyan]Registering server '{server_name}' at {server_url}...[/cyan]") |
| 224 | + result = make_authenticated_request("POST", "/gateways", json_data=registration_data) |
| 225 | + registered_server_id = result.get("id") |
| 226 | + console.print(f"[green]✓ Server registered successfully (ID: {registered_server_id})[/green]") |
| 227 | + |
| 228 | + # Set up cleanup for temporary servers |
| 229 | + if temporary and registered_server_id: |
| 230 | + |
| 231 | + def cleanup_server(): |
| 232 | + """Unregister the server on exit.""" |
| 233 | + if registered_server_id: |
| 234 | + try: |
| 235 | + console.print(f"\n[cyan]Unregistering temporary server (ID: {registered_server_id})...[/cyan]") |
| 236 | + make_authenticated_request("DELETE", f"/gateways/{registered_server_id}") |
| 237 | + console.print("[green]✓ Server unregistered successfully[/green]") |
| 238 | + except Exception as e: |
| 239 | + console.print(f"[yellow]Warning: Failed to unregister server: {e}[/yellow]") |
| 240 | + |
| 241 | + # Register cleanup handlers |
| 242 | + atexit.register(cleanup_server) |
| 243 | + |
| 244 | + except Exception as e: |
| 245 | + console.print(f"[yellow]Warning: Failed to register server: {e}[/yellow]") |
| 246 | + console.print("[yellow]Continuing without registration...[/yellow]") |
| 247 | + |
| 248 | + # Wait for the process to terminate |
| 249 | + proc.join() |
0 commit comments