Skip to content

Commit a791afe

Browse files
author
User
committed
stdio working
1 parent 4b68a44 commit a791afe

File tree

1 file changed

+134
-31
lines changed

1 file changed

+134
-31
lines changed

src/mcpm/commands/share.py

Lines changed: 134 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,13 @@
33
"""
44

55
import secrets
6+
import shlex
7+
import shutil
68
import signal
79
import subprocess
810
import sys
911
import time
12+
from typing import Optional, Tuple
1013

1114
import click
1215
from rich.console import Console
@@ -17,21 +20,133 @@
1720
console = Console()
1821

1922

23+
def find_mcp_proxy() -> Optional[str]:
24+
"""Find the mcp-proxy executable in PATH."""
25+
return shutil.which("mcp-proxy")
26+
27+
28+
def start_mcp_proxy(command: str, port: Optional[int] = None) -> Tuple[subprocess.Popen, int]:
29+
"""
30+
Start mcp-proxy to convert a stdio MCP server to an SSE server.
31+
32+
Args:
33+
command: The command to run the stdio MCP server
34+
port: The port for the SSE server (random if None)
35+
36+
Returns:
37+
A tuple of (process, port)
38+
"""
39+
mcp_proxy_path = find_mcp_proxy()
40+
if not mcp_proxy_path:
41+
console.print("[bold red]Error:[/] mcp-proxy not found in PATH")
42+
console.print("Please install mcp-proxy using one of the following methods:")
43+
console.print(" - pip install mcp-proxy")
44+
console.print(" - uv tool install mcp-proxy")
45+
console.print(" - npx -y @smithery/cli install mcp-proxy")
46+
sys.exit(1)
47+
48+
# Build the mcp-proxy command
49+
cmd_parts = [mcp_proxy_path]
50+
51+
# Add port if specified
52+
if port:
53+
cmd_parts.extend(["--sse-port", str(port)])
54+
55+
# Add the command to run the stdio server using -- separator
56+
cmd_parts.append("--")
57+
cmd_parts.extend(shlex.split(command))
58+
59+
# Start mcp-proxy as a subprocess
60+
console.print(f"[cyan]Running command: [bold]{' '.join(cmd_parts)}[/bold][/]")
61+
process = subprocess.Popen(cmd_parts, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, bufsize=1)
62+
63+
# If port is None, we need to parse the output to find the random port
64+
actual_port = port
65+
if not actual_port:
66+
# Wait for mcp-proxy to output the port information
67+
start_time = time.time()
68+
port_found = False
69+
70+
while time.time() - start_time < 15: # Extended timeout
71+
if process.stderr:
72+
line = process.stderr.readline()
73+
if line:
74+
print(line, end="")
75+
# Look for different possible port indicators in the output
76+
if "Serving on" in line:
77+
try:
78+
actual_port = int(line.split(":")[-1].strip())
79+
port_found = True
80+
break
81+
except (ValueError, IndexError):
82+
pass
83+
elif "Uvicorn running on http://" in line:
84+
try:
85+
url_part = line.split("Uvicorn running on ")[1].split(" ")[0]
86+
actual_port = int(url_part.split(":")[-1].strip())
87+
port_found = True
88+
break
89+
except (ValueError, IndexError):
90+
pass
91+
92+
if process.stdout:
93+
line = process.stdout.readline()
94+
if line:
95+
print(line, end="")
96+
# Also check stdout for port information
97+
if "Uvicorn running on http://" in line:
98+
try:
99+
url_part = line.split("Uvicorn running on ")[1].split(" ")[0]
100+
actual_port = int(url_part.split(":")[-1].strip())
101+
port_found = True
102+
break
103+
except (ValueError, IndexError):
104+
pass
105+
106+
if process.poll() is not None:
107+
# Process terminated prematurely
108+
stderr_output = process.stderr.read() if process.stderr else ""
109+
console.print("[bold red]Error:[/] mcp-proxy terminated unexpectedly")
110+
console.print(f"[red]Error output:[/]\n{stderr_output}")
111+
sys.exit(1)
112+
113+
time.sleep(0.1)
114+
115+
if not port_found and not actual_port:
116+
console.print("[bold red]Error:[/] Could not determine the port mcp-proxy is running on")
117+
process.terminate()
118+
sys.exit(1)
119+
120+
if not actual_port:
121+
console.print("[bold red]Error:[/] Could not determine the port mcp-proxy is running on")
122+
process.terminate()
123+
sys.exit(1)
124+
125+
return process, actual_port
126+
127+
20128
@click.command()
21129
@click.argument("command", type=str)
22-
@click.option("--port", type=int, default=8080, help="Port the SSE server listens on")
23-
@click.option("--address", type=str, default=None, help="Remote address for tunnel")
24-
@click.option("--http", is_flag=True, default=False, help="Use HTTP instead of HTTPS")
130+
@click.option("--port", type=int, default=None, help="Port for the SSE server (random if not specified)")
131+
@click.option("--address", type=str, default=None, help="Remote address for tunnel, use share.mcpm.sh if not specified")
132+
@click.option(
133+
"--http", is_flag=True, default=False, help="Use HTTP instead of HTTPS. NOT recommended to use on public networks."
134+
)
25135
@click.help_option("-h", "--help")
26136
def share(command, port, address, http):
27-
"""Share an MCP SSE server command through a tunnel.
137+
"""Share an MCP server through a tunnel.
138+
139+
This command uses mcp-proxy to expose a stdio MCP server as an SSE server,
140+
then creates a tunnel to make it accessible remotely.
28141
29142
COMMAND is the shell command to run the MCP server.
30143
31-
Example:
144+
Examples:
32145
33-
mcpm share "python server.py"
34-
mcpm share "uvicorn myserver:app" --port 5000
146+
\b
147+
mcpm share "python stdio_server.py"
148+
mcpm share "npx mcp-server" --port 5000
149+
mcpm share "uv run my-mcp-server" --address myserver.com:7000
35150
"""
36151
# Default to standard share address if not specified
37152
if not address:
@@ -42,43 +157,31 @@ def share(command, port, address, http):
42157
remote_host, remote_port = address.split(":")
43158
remote_port = int(remote_port)
44159

45-
# Start the server process
46-
console.print(f"[cyan]Starting server with command: [bold]{command}[/bold][/]")
47-
server_process = subprocess.Popen(
48-
command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, bufsize=1
49-
)
50-
51-
# Wait a moment for the server to start
52-
console.print(f"[cyan]Waiting for server to start on port {port}...[/]")
53-
time.sleep(2) # Allow the server to start
54-
55-
# Check if the process is still running
56-
if server_process.poll() is not None:
57-
console.print("[bold red]Error:[/] Server process terminated unexpectedly")
58-
# Print stderr output to help with debugging
59-
stderr_output = server_process.stderr.read()
60-
console.print(f"[red]Server error output:[/]\n{stderr_output}")
61-
return
62-
63-
# Create and start the tunnel
160+
# Start mcp-proxy to convert stdio to SSE
161+
console.print(f"[cyan]Starting mcp-proxy with command: [bold]{command}[/bold][/]")
64162
try:
65-
console.print(f"[cyan]Creating tunnel from localhost:{port} to {remote_host}:{remote_port}...[/]")
163+
server_process, actual_port = start_mcp_proxy(command, port)
164+
console.print(f"[cyan]mcp-proxy SSE server running on port [bold]{actual_port}[/bold][/]")
165+
166+
# Create and start the tunnel
167+
console.print(f"[cyan]Creating tunnel from localhost:{actual_port} to {remote_host}:{remote_port}...[/]")
66168
# Generate a random token for security
67169
share_token = secrets.token_urlsafe(32)
68170
tunnel = Tunnel(
69171
remote_host=remote_host,
70172
remote_port=remote_port,
71173
local_host="localhost",
72-
local_port=port,
174+
local_port=actual_port,
73175
share_token=share_token,
74176
http=http,
75177
share_server_tls_certificate=None,
76178
)
77179

78180
share_url = tunnel.start_tunnel()
79181

80-
# Display the share URL
81-
console.print(f"[bold green]Server is now shared at: [/][bold cyan]{share_url}[/]")
182+
# Display the share URL - append /sse for mcp-proxy's SSE endpoint
183+
sse_url = f"{share_url}/sse"
184+
console.print(f"[bold green]Server is now shared at: [/][bold cyan]{sse_url}[/]")
82185
console.print("[yellow]Press Ctrl+C to stop sharing and terminate the server[/]")
83186

84187
# Handle cleanup on termination signals
@@ -118,7 +221,7 @@ def signal_handler(sig, frame):
118221
except Exception as e:
119222
console.print(f"[bold red]Error:[/] {str(e)}")
120223
# Clean up
121-
if server_process.poll() is None:
224+
if "server_process" in locals() and server_process.poll() is None:
122225
server_process.terminate()
123226
server_process.wait(timeout=5)
124227
if "tunnel" in locals():

0 commit comments

Comments
 (0)