Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 49 additions & 18 deletions src/mcpm/commands/profile/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,19 +42,29 @@ async def find_available_port(preferred_port, max_attempts=10):
return preferred_port


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

try:
# Create FastMCP proxy for profile servers
if sse_mode:
action = "profile_run_sse"
elif http_mode:
action = "profile_run_http"
else:
action = "profile_run"

proxy = await create_mcpm_proxy(
servers=profile_servers,
name=f"profile-{profile_name}",
stdio_mode=not http_mode, # stdio_mode=False for HTTP
action="profile_run",
stdio_mode=not (http_mode or sse_mode), # stdio_mode=False for HTTP/SSE
action=action,
profile_name=profile_name,
)

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

# Note: Usage tracking is handled by proxy middleware

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

# Display profile information in a nice panel
http_url = f"http://{host}:{actual_port}/mcp/"
if sse_mode:
server_url = f"http://{host}:{actual_port}/sse/"
title = "📡 SSE Profile Running"
else:
server_url = f"http://{host}:{actual_port}/mcp/"
title = "📁 Profile Running Locally"

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

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[/]"
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[/]"

panel = Panel(
panel_content,
title="📁 Profile Running Locally",
title=title,
title_align="left",
border_style="green",
padding=(1, 2),
)
console.print(panel)

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

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

except KeyboardInterrupt:
logger.info("Profile execution interrupted")
if http_mode or sse_mode:
logger.warning("\nProfile execution interrupted")
return 130
except Exception as e:
logger.error(f"Error running profile '{profile_name}': {e}")
Expand All @@ -115,11 +134,12 @@ async def run_profile_fastmcp(profile_servers, profile_name, http_mode=False, po
@click.command()
@click.argument("profile_name")
@click.option("--http", is_flag=True, help="Run profile over HTTP instead of stdio")
@click.option("--port", type=int, default=DEFAULT_PORT, help=f"Port for HTTP mode (default: {DEFAULT_PORT})")
@click.option("--host", type=str, default="127.0.0.1", help="Host address for HTTP mode (default: 127.0.0.1)")
@click.option("--sse", is_flag=True, help="Run profile over SSE instead of stdio")
@click.option("--port", type=int, default=DEFAULT_PORT, help=f"Port for HTTP / SSE mode (default: {DEFAULT_PORT})")
@click.option("--host", type=str, default="127.0.0.1", help="Host address for HTTP / SSE mode (default: 127.0.0.1)")
@click.help_option("-h", "--help")
def run(profile_name, http, port, host):
"""Execute all servers in a profile over stdio or HTTP.
def run(profile_name, http, sse, port, host):
"""Execute all servers in a profile over stdio, HTTP, or SSE.

Uses FastMCP proxy to aggregate servers into a unified MCP interface
with proper capability namespacing. By default runs over stdio.
Expand All @@ -129,7 +149,9 @@ def run(profile_name, http, port, host):
\b
mcpm profile run web-dev # Run over stdio (default)
mcpm profile run --http web-dev # Run over HTTP on 127.0.0.1:6276
mcpm profile run --sse web-dev # Run over SSE on 127.0.0.1:6276
mcpm profile run --http --port 9000 ai # Run over HTTP on 127.0.0.1:9000
mcpm profile run --sse --port 9000 ai # Run over SSE on 127.0.0.1:9000
mcpm profile run --http --host 0.0.0.0 web-dev # Run over HTTP on 0.0.0.0:6276

Debug logging: Set MCPM_DEBUG=1 for verbose output
Expand All @@ -141,6 +163,11 @@ def run(profile_name, http, port, host):

profile_name = profile_name.strip()

# Validate mutually exclusive options
if http and sse:
logger.error("Error: Cannot use both --http and --sse flags together")
return 1

# Check if profile exists
try:
profile_servers = profile_config_manager.get_profile(profile_name)
Expand Down Expand Up @@ -169,8 +196,12 @@ def run(profile_name, http, port, host):

# Use FastMCP proxy for all cases (single or multiple servers)
logger.debug(f"Using FastMCP proxy for {len(profile_servers)} server(s)")
if http:
logger.debug(f"HTTP mode on port {port}")
mode = "SSE" if sse else "HTTP" if http else "stdio"
logger.debug(f"Mode: {mode}")
if http or sse:
logger.debug(f"Port: {port}")

# Run the async function
return asyncio.run(run_profile_fastmcp(profile_servers, profile_name, http_mode=http, port=port, host=host))
return asyncio.run(
run_profile_fastmcp(profile_servers, profile_name, http_mode=http, sse_mode=sse, port=port, host=host)
)
77 changes: 55 additions & 22 deletions src/mcpm/commands/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,9 @@ def find_installed_server(server_name):
return None, None


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

# Create FastMCP proxy for single server
action = "run_http" if http_mode else "run"
if sse_mode:
action = "run_sse"
elif http_mode:
action = "run_http"
else:
action = "run"

proxy = await create_mcpm_proxy(
servers=[server_config],
name=f"mcpm-run-{server_name}",
stdio_mode=not http_mode, # stdio_mode=False for HTTP
stdio_mode=not (http_mode or sse_mode), # stdio_mode=False for HTTP/SSE
action=action,
)

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

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

# Display server information in a nice panel
http_url = f"http://{host}:{actual_port}/mcp/"
panel_content = f"[bold]Server:[/] {server_name}\n[bold]URL:[/] [cyan]{http_url}[/cyan]\n\n[dim]Press Ctrl+C to stop the server[/]"
panel = Panel(
panel_content, title="🌐 Local Server Running", title_align="left", border_style="green", padding=(1, 2)
)
if sse_mode:
server_url = f"http://{host}:{actual_port}/sse/"
title = "📡 SSE Server Running"
else:
server_url = f"http://{host}:{actual_port}/mcp/"
title = "🌐 Local Server Running"

panel_content = f"[bold]Server:[/] {server_name}\n[bold]URL:[/] [cyan]{server_url}[/cyan]\n\n[dim]Press Ctrl+C to stop the server[/]"
panel = Panel(panel_content, title=title, title_align="left", border_style="green", padding=(1, 2))
console.print(panel)

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

# Run FastMCP proxy in HTTP mode with uvicorn logging control
# Run FastMCP proxy in HTTP/SSE mode with uvicorn logging control
transport = "sse" if sse_mode else "http"
await proxy.run_http_async(
host=host, port=actual_port, show_banner=False, uvicorn_config={"log_level": get_uvicorn_log_level()}
host=host,
port=actual_port,
show_banner=False,
transport=transport,
uvicorn_config={"log_level": get_uvicorn_log_level()},
)
else:
# Run FastMCP proxy in stdio mode (default)
Expand All @@ -84,7 +102,7 @@ async def run_server_with_fastmcp(server_config, server_name, http_mode=False, p

except KeyboardInterrupt:
logger.info("Server execution interrupted")
if http_mode:
if http_mode or sse_mode:
logger.warning("\nServer execution interrupted")
return 130
except Exception as e:
Expand Down Expand Up @@ -114,19 +132,23 @@ async def find_available_port(preferred_port, max_attempts=10):
@click.command()
@click.argument("server_name")
@click.option("--http", is_flag=True, help="Run server over HTTP instead of stdio")
@click.option("--port", type=int, default=DEFAULT_PORT, help=f"Port for HTTP mode (default: {DEFAULT_PORT})")
@click.option("--host", type=str, default="127.0.0.1", help="Host address for HTTP mode (default: 127.0.0.1)")
@click.option("--sse", is_flag=True, help="Run server over SSE instead of stdio")
@click.option("--port", type=int, default=DEFAULT_PORT, help=f"Port for HTTP / SSE mode (default: {DEFAULT_PORT})")
@click.option("--host", type=str, default="127.0.0.1", help="Host address for HTTP / SSE mode (default: 127.0.0.1)")
@click.help_option("-h", "--help")
def run(server_name, http, port, host):
"""Execute a server from global configuration over stdio or HTTP.
def run(server_name, http, sse, port, host):
"""Execute a server from global configuration over stdio, HTTP, or SSE.

Runs an installed MCP server from the global configuration. By default
runs over stdio for client communication, but can run over HTTP with --http.
runs over stdio for client communication, but can run over HTTP with --http
or over SSE with --sse.

Examples:
mcpm run mcp-server-browse # Run over stdio (default)
mcpm run --http mcp-server-browse # Run over HTTP on 127.0.0.1:6276
mcpm run --sse mcp-server-browse # Run over SSE on 127.0.0.1:6276
mcpm run --http --port 9000 filesystem # Run over HTTP on 127.0.0.1:9000
mcpm run --sse --port 9000 filesystem # Run over SSE on 127.0.0.1:9000
mcpm run --http --host 0.0.0.0 filesystem # Run over HTTP on 0.0.0.0:6276

Note: stdio mode is typically used in MCP client configurations:
Expand Down Expand Up @@ -164,20 +186,31 @@ def run(server_name, http, port, host):
if server_config.headers:
logger.debug(f"Headers: {list(server_config.headers.keys())}")

logger.debug(f"Mode: {'HTTP' if http else 'stdio'}")
if http:
# Validate mutually exclusive options
if http and sse:
logger.error("Error: Cannot use both --http and --sse flags together")
sys.exit(1)

mode = "SSE" if sse else "HTTP" if http else "stdio"
logger.debug(f"Mode: {mode}")
if http or sse:
logger.debug(f"Port: {port}")

# Choose execution method
if http:
# Use FastMCP proxy for HTTP mode
exit_code = asyncio.run(
run_server_with_fastmcp(server_config, server_name, http_mode=True, port=port, host=host)
run_server_with_fastmcp(server_config, server_name, http_mode=True, sse_mode=False, port=port, host=host)
)
elif sse:
# Use FastMCP proxy for SSE mode
exit_code = asyncio.run(
run_server_with_fastmcp(server_config, server_name, http_mode=False, sse_mode=True, port=port, host=host)
)
else:
# Use FastMCP proxy for stdio mode (enables middleware and usage tracking)
exit_code = asyncio.run(
run_server_with_fastmcp(server_config, server_name, http_mode=False, port=port, host=host)
run_server_with_fastmcp(server_config, server_name, http_mode=False, sse_mode=False, port=port, host=host)
)

sys.exit(exit_code)
Loading