Skip to content

Commit f70d45b

Browse files
committed
feat:sse support for 'mcpm profile run'
1 parent b818c67 commit f70d45b

File tree

1 file changed

+49
-18
lines changed
  • src/mcpm/commands/profile

1 file changed

+49
-18
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+
)

0 commit comments

Comments
 (0)