Skip to content

Commit 62badd9

Browse files
committed
feat: Add the ability to auto-register and unregister wrapped servers
#15 Branch: Run-15 Signed-off-by: Gabe Goodhart <ghart@us.ibm.com>
1 parent 834f12f commit 62badd9

1 file changed

Lines changed: 117 additions & 7 deletions

File tree

cforge/commands/server/run.py

Lines changed: 117 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,18 @@
1212
"""
1313

1414
# Standard
15+
import atexit
16+
import multiprocessing
17+
import os
18+
import time
1519
from typing import List, Optional
1620

1721
# Third-Party
22+
import requests
1823
import typer
1924

2025
# First-Party
21-
from mcpgateway.translate import main as translate_main
26+
from cforge.common import get_console, make_authenticated_request
2227

2328

2429
def run(
@@ -55,24 +60,45 @@ def run(
5560
),
5661
stateless: bool = typer.Option(False, "--stateless", help="Use stateless mode for streamable HTTP (default: False)"),
5762
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"),
5868
) -> None:
5969
"""Run MCP servers locally and expose them via SSE or streamable HTTP.
6070
6171
This command bridges between different MCP transport protocols: stdio/JSON-RPC,
6272
HTTP/SSE, and streamable HTTP. It enables exposing local MCP servers over HTTP
6373
or consuming remote endpoints as local stdio servers.
6474
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+
6579
Examples:
6680
67-
# Expose a local MCP server via SSE
81+
# Expose a local MCP server via SSE (auto-registered)
6882
cforge run --stdio "uvx mcp-server-git" --port 9000
6983
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+
7090
# Expose via both SSE and streamable HTTP
7191
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
7592
"""
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+
76102
# Build argument list for translate_main
77103
args = []
78104

@@ -135,5 +161,89 @@ def run(
135161
if json_response:
136162
args.append("--jsonResponse")
137163

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

Comments
 (0)