33"""
44
55import secrets
6+ import shlex
7+ import shutil
68import signal
79import subprocess
810import sys
911import time
12+ from typing import Optional , Tuple
1013
1114import click
1215from rich .console import Console
1720console = 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" )
26136def 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