Skip to content

Commit 4b68a44

Browse files
author
User
committed
feat(share): add simple share command
Add simple share command to reverse proxy a single local mcp server. For easier access to local mcp server for development.
1 parent 4567486 commit 4b68a44

File tree

2 files changed

+128
-0
lines changed

2 files changed

+128
-0
lines changed

src/mcpm/cli.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
stash,
2525
transfer,
2626
)
27+
from mcpm.commands.share import share
2728

2829
console = Console()
2930
client_config_manager = ClientConfigManager()
@@ -158,6 +159,7 @@ def main(ctx, help_flag, version):
158159
commands_table.add_row(" [cyan]ls[/]", "List all installed MCP servers.")
159160
commands_table.add_row(" [cyan]stash[/]", "Temporarily store a server configuration aside.")
160161
commands_table.add_row(" [cyan]pop[/]", "Restore a previously stashed server configuration.")
162+
commands_table.add_row(" [cyan]share[/]", "Share a single MCP server through a tunnel.")
161163

162164
commands_table.add_row("[yellow]profile[/]")
163165
commands_table.add_row(" [cyan]profile[/]", "Manage MCPM profiles.")
@@ -197,6 +199,7 @@ def main(ctx, help_flag, version):
197199
main.add_command(profile.deactivate)
198200
main.add_command(router.router, name="router")
199201
main.add_command(custom.import_server, name="import")
202+
main.add_command(share)
200203

201204
if __name__ == "__main__":
202205
main()

src/mcpm/commands/share.py

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
"""
2+
Share command for MCPM - Share a single MCP server through a tunnel
3+
"""
4+
5+
import secrets
6+
import signal
7+
import subprocess
8+
import sys
9+
import time
10+
11+
import click
12+
from rich.console import Console
13+
14+
from mcpm.router.share import Tunnel
15+
from mcpm.utils.config import DEFAULT_SHARE_ADDRESS
16+
17+
console = Console()
18+
19+
20+
@click.command()
21+
@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")
25+
@click.help_option("-h", "--help")
26+
def share(command, port, address, http):
27+
"""Share an MCP SSE server command through a tunnel.
28+
29+
COMMAND is the shell command to run the MCP server.
30+
31+
Example:
32+
33+
mcpm share "python server.py"
34+
mcpm share "uvicorn myserver:app" --port 5000
35+
"""
36+
# Default to standard share address if not specified
37+
if not address:
38+
address = DEFAULT_SHARE_ADDRESS
39+
console.print(f"[cyan]Using default share address: {address}[/]")
40+
41+
# Split remote host and port
42+
remote_host, remote_port = address.split(":")
43+
remote_port = int(remote_port)
44+
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
64+
try:
65+
console.print(f"[cyan]Creating tunnel from localhost:{port} to {remote_host}:{remote_port}...[/]")
66+
# Generate a random token for security
67+
share_token = secrets.token_urlsafe(32)
68+
tunnel = Tunnel(
69+
remote_host=remote_host,
70+
remote_port=remote_port,
71+
local_host="localhost",
72+
local_port=port,
73+
share_token=share_token,
74+
http=http,
75+
share_server_tls_certificate=None,
76+
)
77+
78+
share_url = tunnel.start_tunnel()
79+
80+
# Display the share URL
81+
console.print(f"[bold green]Server is now shared at: [/][bold cyan]{share_url}[/]")
82+
console.print("[yellow]Press Ctrl+C to stop sharing and terminate the server[/]")
83+
84+
# Handle cleanup on termination signals
85+
def signal_handler(sig, frame):
86+
console.print("\n[yellow]Terminating server and tunnel...[/]")
87+
tunnel.kill()
88+
if server_process.poll() is None:
89+
server_process.terminate()
90+
server_process.wait(timeout=5)
91+
sys.exit(0)
92+
93+
# Register signal handlers
94+
signal.signal(signal.SIGINT, signal_handler)
95+
signal.signal(signal.SIGTERM, signal_handler)
96+
97+
# Keep the main process running and display server output
98+
while True:
99+
if server_process.poll() is not None:
100+
console.print("[bold red]Server process terminated unexpectedly[/]")
101+
tunnel.kill()
102+
break
103+
104+
# Print server output
105+
if server_process.stdout:
106+
line = server_process.stdout.readline()
107+
if line:
108+
print(line, end="")
109+
110+
# Also check stderr
111+
if server_process.stderr:
112+
line = server_process.stderr.readline()
113+
if line:
114+
print(line, end="")
115+
116+
time.sleep(0.1)
117+
118+
except Exception as e:
119+
console.print(f"[bold red]Error:[/] {str(e)}")
120+
# Clean up
121+
if server_process.poll() is None:
122+
server_process.terminate()
123+
server_process.wait(timeout=5)
124+
if "tunnel" in locals():
125+
tunnel.kill()

0 commit comments

Comments
 (0)