Skip to content

Commit e66ea87

Browse files
add support sse for run command (#214)
* feat:sse support for 'mcpm run' * feat:sse support for 'mcpm profile run'
1 parent 2427bff commit e66ea87

File tree

2 files changed

+104
-40
lines changed

2 files changed

+104
-40
lines changed

src/mcpm/commands/profile/run.py

Lines changed: 49 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -42,19 +42,29 @@ async def find_available_port(preferred_port, max_attempts=10):
4242
return preferred_port
4343

4444

45-
async def run_profile_fastmcp(profile_servers, profile_name, http_mode=False, port=DEFAULT_PORT, host="127.0.0.1"):
45+
async def run_profile_fastmcp(
46+
profile_servers, profile_name, http_mode=False, sse_mode=False, port=DEFAULT_PORT, host="127.0.0.1"
47+
):
4648
"""Run profile servers using FastMCP proxy for proper aggregation."""
4749
server_count = len(profile_servers)
4850
logger.debug(f"Using FastMCP proxy to aggregate {server_count} server(s)")
49-
logger.debug(f"Mode: {'HTTP' if http_mode else 'stdio'}")
51+
mode = "SSE" if sse_mode else "HTTP" if http_mode else "stdio"
52+
logger.debug(f"Mode: {mode}")
5053

5154
try:
5255
# Create FastMCP proxy for profile servers
56+
if sse_mode:
57+
action = "profile_run_sse"
58+
elif http_mode:
59+
action = "profile_run_http"
60+
else:
61+
action = "profile_run"
62+
5363
proxy = await create_mcpm_proxy(
5464
servers=profile_servers,
5565
name=f"profile-{profile_name}",
56-
stdio_mode=not http_mode, # stdio_mode=False for HTTP
57-
action="profile_run",
66+
stdio_mode=not (http_mode or sse_mode), # stdio_mode=False for HTTP/SSE
67+
action=action,
5868
profile_name=profile_name,
5969
)
6070

@@ -68,34 +78,41 @@ async def run_profile_fastmcp(profile_servers, profile_name, http_mode=False, po
6878

6979
# Note: Usage tracking is handled by proxy middleware
7080

71-
if http_mode:
81+
if http_mode or sse_mode:
7282
# Try to find an available port if the requested one is taken
7383
actual_port = await find_available_port(port)
7484
if actual_port != port:
7585
logger.debug(f"Port {port} is busy, using port {actual_port} instead")
7686

7787
# Display profile information in a nice panel
78-
http_url = f"http://{host}:{actual_port}/mcp/"
88+
if sse_mode:
89+
server_url = f"http://{host}:{actual_port}/sse/"
90+
title = "📡 SSE Profile Running"
91+
else:
92+
server_url = f"http://{host}:{actual_port}/mcp/"
93+
title = "📁 Profile Running Locally"
7994

8095
# Build server list
8196
server_list = "\n".join([f" • [cyan]{server.name}[/]" for server in profile_servers])
8297

83-
panel_content = f"[bold]Profile:[/] {profile_name}\n[bold]URL:[/] [cyan]{http_url}[/cyan]\n\n[bold]Servers:[/]\n{server_list}\n\n[dim]Press Ctrl+C to stop the profile[/]"
98+
panel_content = f"[bold]Profile:[/] {profile_name}\n[bold]URL:[/] [cyan]{server_url}[/cyan]\n\n[bold]Servers:[/]\n{server_list}\n\n[dim]Press Ctrl+C to stop the profile[/]"
8499

85100
panel = Panel(
86101
panel_content,
87-
title="📁 Profile Running Locally",
102+
title=title,
88103
title_align="left",
89104
border_style="green",
90105
padding=(1, 2),
91106
)
92107
console.print(panel)
93108

94-
logger.debug(f"Starting FastMCP proxy for profile '{profile_name}' on {host}:{actual_port}")
109+
mode = "SSE" if sse_mode else "HTTP"
110+
logger.debug(f"Starting FastMCP proxy for profile '{profile_name}' in {mode} mode on {host}:{actual_port}")
95111

96-
# Run the aggregated proxy over HTTP with uvicorn logging control
112+
# Run the aggregated proxy over HTTP/SSE with uvicorn logging control
113+
transport = "sse" if sse_mode else "http"
97114
await proxy.run_http_async(
98-
host=host, port=actual_port, uvicorn_config={"log_level": get_uvicorn_log_level()}
115+
host=host, port=actual_port, transport=transport, uvicorn_config={"log_level": get_uvicorn_log_level()}
99116
)
100117
else:
101118
# Run the aggregated proxy over stdio (default)
@@ -106,6 +123,8 @@ async def run_profile_fastmcp(profile_servers, profile_name, http_mode=False, po
106123

107124
except KeyboardInterrupt:
108125
logger.info("Profile execution interrupted")
126+
if http_mode or sse_mode:
127+
logger.warning("\nProfile execution interrupted")
109128
return 130
110129
except Exception as e:
111130
logger.error(f"Error running profile '{profile_name}': {e}")
@@ -115,11 +134,12 @@ async def run_profile_fastmcp(profile_servers, profile_name, http_mode=False, po
115134
@click.command()
116135
@click.argument("profile_name")
117136
@click.option("--http", is_flag=True, help="Run profile over HTTP instead of stdio")
118-
@click.option("--port", type=int, default=DEFAULT_PORT, help=f"Port for HTTP mode (default: {DEFAULT_PORT})")
119-
@click.option("--host", type=str, default="127.0.0.1", help="Host address for HTTP mode (default: 127.0.0.1)")
137+
@click.option("--sse", is_flag=True, help="Run profile over SSE instead of stdio")
138+
@click.option("--port", type=int, default=DEFAULT_PORT, help=f"Port for HTTP / SSE mode (default: {DEFAULT_PORT})")
139+
@click.option("--host", type=str, default="127.0.0.1", help="Host address for HTTP / SSE mode (default: 127.0.0.1)")
120140
@click.help_option("-h", "--help")
121-
def run(profile_name, http, port, host):
122-
"""Execute all servers in a profile over stdio or HTTP.
141+
def run(profile_name, http, sse, port, host):
142+
"""Execute all servers in a profile over stdio, HTTP, or SSE.
123143
124144
Uses FastMCP proxy to aggregate servers into a unified MCP interface
125145
with proper capability namespacing. By default runs over stdio.
@@ -129,7 +149,9 @@ def run(profile_name, http, port, host):
129149
\b
130150
mcpm profile run web-dev # Run over stdio (default)
131151
mcpm profile run --http web-dev # Run over HTTP on 127.0.0.1:6276
152+
mcpm profile run --sse web-dev # Run over SSE on 127.0.0.1:6276
132153
mcpm profile run --http --port 9000 ai # Run over HTTP on 127.0.0.1:9000
154+
mcpm profile run --sse --port 9000 ai # Run over SSE on 127.0.0.1:9000
133155
mcpm profile run --http --host 0.0.0.0 web-dev # Run over HTTP on 0.0.0.0:6276
134156
135157
Debug logging: Set MCPM_DEBUG=1 for verbose output
@@ -141,6 +163,11 @@ def run(profile_name, http, port, host):
141163

142164
profile_name = profile_name.strip()
143165

166+
# Validate mutually exclusive options
167+
if http and sse:
168+
logger.error("Error: Cannot use both --http and --sse flags together")
169+
return 1
170+
144171
# Check if profile exists
145172
try:
146173
profile_servers = profile_config_manager.get_profile(profile_name)
@@ -169,8 +196,12 @@ def run(profile_name, http, port, host):
169196

170197
# Use FastMCP proxy for all cases (single or multiple servers)
171198
logger.debug(f"Using FastMCP proxy for {len(profile_servers)} server(s)")
172-
if http:
173-
logger.debug(f"HTTP mode on port {port}")
199+
mode = "SSE" if sse else "HTTP" if http else "stdio"
200+
logger.debug(f"Mode: {mode}")
201+
if http or sse:
202+
logger.debug(f"Port: {port}")
174203

175204
# Run the async function
176-
return asyncio.run(run_profile_fastmcp(profile_servers, profile_name, http_mode=http, port=port, host=host))
205+
return asyncio.run(
206+
run_profile_fastmcp(profile_servers, profile_name, http_mode=http, sse_mode=sse, port=port, host=host)
207+
)

src/mcpm/commands/run.py

Lines changed: 55 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,9 @@ def find_installed_server(server_name):
3232
return None, None
3333

3434

35-
async def run_server_with_fastmcp(server_config, server_name, http_mode=False, port=None, host="127.0.0.1"):
35+
async def run_server_with_fastmcp(
36+
server_config, server_name, http_mode=False, sse_mode=False, port=None, host="127.0.0.1"
37+
):
3638
"""Run server using FastMCP proxy (stdio or HTTP)."""
3739
try:
3840
# Use default port if none specified
@@ -41,11 +43,17 @@ async def run_server_with_fastmcp(server_config, server_name, http_mode=False, p
4143
# Note: Usage tracking is handled by proxy middleware
4244

4345
# Create FastMCP proxy for single server
44-
action = "run_http" if http_mode else "run"
46+
if sse_mode:
47+
action = "run_sse"
48+
elif http_mode:
49+
action = "run_http"
50+
else:
51+
action = "run"
52+
4553
proxy = await create_mcpm_proxy(
4654
servers=[server_config],
4755
name=f"mcpm-run-{server_name}",
48-
stdio_mode=not http_mode, # stdio_mode=False for HTTP
56+
stdio_mode=not (http_mode or sse_mode), # stdio_mode=False for HTTP/SSE
4957
action=action,
5058
)
5159

@@ -55,25 +63,35 @@ async def run_server_with_fastmcp(server_config, server_name, http_mode=False, p
5563
# Re-suppress library logging after FastMCP initialization
5664
ensure_dependency_logging_suppressed()
5765

58-
if http_mode:
66+
if http_mode or sse_mode:
5967
# Try to find an available port if the requested one is taken
6068
actual_port = await find_available_port(port)
6169
if actual_port != port:
6270
logger.debug(f"Port {port} is busy, using port {actual_port} instead")
6371

6472
# Display server information in a nice panel
65-
http_url = f"http://{host}:{actual_port}/mcp/"
66-
panel_content = f"[bold]Server:[/] {server_name}\n[bold]URL:[/] [cyan]{http_url}[/cyan]\n\n[dim]Press Ctrl+C to stop the server[/]"
67-
panel = Panel(
68-
panel_content, title="🌐 Local Server Running", title_align="left", border_style="green", padding=(1, 2)
69-
)
73+
if sse_mode:
74+
server_url = f"http://{host}:{actual_port}/sse/"
75+
title = "📡 SSE Server Running"
76+
else:
77+
server_url = f"http://{host}:{actual_port}/mcp/"
78+
title = "🌐 Local Server Running"
79+
80+
panel_content = f"[bold]Server:[/] {server_name}\n[bold]URL:[/] [cyan]{server_url}[/cyan]\n\n[dim]Press Ctrl+C to stop the server[/]"
81+
panel = Panel(panel_content, title=title, title_align="left", border_style="green", padding=(1, 2))
7082
console.print(panel)
7183

72-
logger.debug(f"Starting FastMCP proxy for server '{server_name}' on {host}:{actual_port}")
84+
mode = "SSE" if sse_mode else "HTTP"
85+
logger.debug(f"Starting FastMCP proxy for server '{server_name}' in {mode} mode on {host}:{actual_port}")
7386

74-
# Run FastMCP proxy in HTTP mode with uvicorn logging control
87+
# Run FastMCP proxy in HTTP/SSE mode with uvicorn logging control
88+
transport = "sse" if sse_mode else "http"
7589
await proxy.run_http_async(
76-
host=host, port=actual_port, show_banner=False, uvicorn_config={"log_level": get_uvicorn_log_level()}
90+
host=host,
91+
port=actual_port,
92+
show_banner=False,
93+
transport=transport,
94+
uvicorn_config={"log_level": get_uvicorn_log_level()},
7795
)
7896
else:
7997
# Run FastMCP proxy in stdio mode (default)
@@ -84,7 +102,7 @@ async def run_server_with_fastmcp(server_config, server_name, http_mode=False, p
84102

85103
except KeyboardInterrupt:
86104
logger.info("Server execution interrupted")
87-
if http_mode:
105+
if http_mode or sse_mode:
88106
logger.warning("\nServer execution interrupted")
89107
return 130
90108
except Exception as e:
@@ -114,19 +132,23 @@ async def find_available_port(preferred_port, max_attempts=10):
114132
@click.command()
115133
@click.argument("server_name")
116134
@click.option("--http", is_flag=True, help="Run server over HTTP instead of stdio")
117-
@click.option("--port", type=int, default=DEFAULT_PORT, help=f"Port for HTTP mode (default: {DEFAULT_PORT})")
118-
@click.option("--host", type=str, default="127.0.0.1", help="Host address for HTTP mode (default: 127.0.0.1)")
135+
@click.option("--sse", is_flag=True, help="Run server over SSE instead of stdio")
136+
@click.option("--port", type=int, default=DEFAULT_PORT, help=f"Port for HTTP / SSE mode (default: {DEFAULT_PORT})")
137+
@click.option("--host", type=str, default="127.0.0.1", help="Host address for HTTP / SSE mode (default: 127.0.0.1)")
119138
@click.help_option("-h", "--help")
120-
def run(server_name, http, port, host):
121-
"""Execute a server from global configuration over stdio or HTTP.
139+
def run(server_name, http, sse, port, host):
140+
"""Execute a server from global configuration over stdio, HTTP, or SSE.
122141
123142
Runs an installed MCP server from the global configuration. By default
124-
runs over stdio for client communication, but can run over HTTP with --http.
143+
runs over stdio for client communication, but can run over HTTP with --http
144+
or over SSE with --sse.
125145
126146
Examples:
127147
mcpm run mcp-server-browse # Run over stdio (default)
128148
mcpm run --http mcp-server-browse # Run over HTTP on 127.0.0.1:6276
149+
mcpm run --sse mcp-server-browse # Run over SSE on 127.0.0.1:6276
129150
mcpm run --http --port 9000 filesystem # Run over HTTP on 127.0.0.1:9000
151+
mcpm run --sse --port 9000 filesystem # Run over SSE on 127.0.0.1:9000
130152
mcpm run --http --host 0.0.0.0 filesystem # Run over HTTP on 0.0.0.0:6276
131153
132154
Note: stdio mode is typically used in MCP client configurations:
@@ -164,20 +186,31 @@ def run(server_name, http, port, host):
164186
if server_config.headers:
165187
logger.debug(f"Headers: {list(server_config.headers.keys())}")
166188

167-
logger.debug(f"Mode: {'HTTP' if http else 'stdio'}")
168-
if http:
189+
# Validate mutually exclusive options
190+
if http and sse:
191+
logger.error("Error: Cannot use both --http and --sse flags together")
192+
sys.exit(1)
193+
194+
mode = "SSE" if sse else "HTTP" if http else "stdio"
195+
logger.debug(f"Mode: {mode}")
196+
if http or sse:
169197
logger.debug(f"Port: {port}")
170198

171199
# Choose execution method
172200
if http:
173201
# Use FastMCP proxy for HTTP mode
174202
exit_code = asyncio.run(
175-
run_server_with_fastmcp(server_config, server_name, http_mode=True, port=port, host=host)
203+
run_server_with_fastmcp(server_config, server_name, http_mode=True, sse_mode=False, port=port, host=host)
204+
)
205+
elif sse:
206+
# Use FastMCP proxy for SSE mode
207+
exit_code = asyncio.run(
208+
run_server_with_fastmcp(server_config, server_name, http_mode=False, sse_mode=True, port=port, host=host)
176209
)
177210
else:
178211
# Use FastMCP proxy for stdio mode (enables middleware and usage tracking)
179212
exit_code = asyncio.run(
180-
run_server_with_fastmcp(server_config, server_name, http_mode=False, port=port, host=host)
213+
run_server_with_fastmcp(server_config, server_name, http_mode=False, sse_mode=False, port=port, host=host)
181214
)
182215

183216
sys.exit(exit_code)

0 commit comments

Comments
 (0)