Skip to content

Commit b929ced

Browse files
feat: router proxy with streamable http server by default (#155)
* feat: router proxy with streamable http server by default * fix: fuck vibe coding
1 parent 0b86e0f commit b929ced

24 files changed

+499
-489
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ mcpm add SERVER_URL --alias ALIAS # Add with a custom alias
9494

9595
# 🛠️ Add custom server
9696
mcpm import stdio SERVER_NAME --command COMMAND --args ARGS --env ENV # Add a stdio MCP server to a client
97-
mcpm import sse SERVER_NAME --url URL # Add a SSE MCP server to a client
97+
mcpm import remote SERVER_NAME --url URL # Add a remote MCP server to a client
9898
mcpm import interact # Add a server by configuring it interactively
9999

100100
# 📋 List and Remove

README.zh-CN.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ mcpm add SERVER_URL --alias ALIAS # 添加并使用自定义别名
128128

129129
# 🛠️ 自定义添加
130130
mcpm import stdio SERVER_NAME --command COMMAND --args ARGS --env ENV # 手动添加一个 stdio MCP 服务器
131-
mcpm import sse SERVER_NAME --url URL # 手动添加一个 SSE MCP 服务器
131+
mcpm import remote SERVER_NAME --url URL # 手动添加一个 remote MCP 服务器
132132
mcpm import interact # 通过交互式添加一个服务器
133133

134134
# 📋 列出和删除

docs/advanced_features.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -91,9 +91,9 @@ The router prefixes capabilities from different servers to avoid conflicts:
9191
- Resources: `webfetch:https://example.com`
9292

9393
#### 4. Server Connections
94-
The router supports both STDIO (command-line) and SSE (HTTP) server connections. For example:
94+
The router supports both STDIO (command-line) and Remote (HTTP and SSE) server connections. For example:
9595
- STDIO: Python Interpreter, Blender
96-
- SSE: Web Search, Notion, Slack
96+
- Remote: Web Search, Notion, Slack
9797

9898
#### 5. Shared Server Sessions
9999
The router maintains persistent connections to all configured servers, allowing multiple clients to share the same server sessions. This means:
@@ -105,9 +105,9 @@ The router maintains persistent connections to all configured servers, allowing
105105
## Features
106106

107107
- Aggregate multiple MCP servers as a single server
108-
- Support both SSE and STDIO connections to underlying servers
108+
- Support both Remote and STDIO connections to underlying servers
109109
- Namespace capabilities from different servers
110-
- Expose a unified SSE server interface
110+
- Expose a unified Remote server interface
111111
- Profile-based server access control
112112
- Dynamic configuration reloading
113113
- Share server connections among multiple clients (no need for separate server instances per client)

docs/router_tech_design.md

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ flowchart TB
4949
```python
5050
import asyncio
5151
from mcpm.router import MCPRouter
52-
from mcpm.core.schema import STDIOServerConfig, SSEServerConfig
52+
from mcpm.core.schema import STDIOServerConfig, RemoteServerConfig
5353

5454
async def main():
5555
# Create a router
@@ -64,16 +64,16 @@ async def main():
6464
)
6565
)
6666

67-
# Add an SSE server
67+
# Add an Remote server
6868
await router.add_server(
6969
"example2",
70-
SSEServerConfig(
71-
url="http://localhost:3000/sse"
70+
RemoteServerConfig(
71+
url="http://localhost:3000/"
7272
)
7373
)
7474

75-
# Start the SSE server
76-
await router.start_sse_server(host="localhost", port=8080)
75+
# Start the Remote server
76+
await router.start_remote_server(host="localhost", port=8080)
7777

7878
if __name__ == "__main__":
7979
asyncio.run(main())
@@ -99,11 +99,11 @@ The main orchestrator class that provides a unified API for the application:
9999

100100
Manages individual connections to downstream MCP servers:
101101
- Handles connection lifecycle for a single server
102-
- Supports different transport types (STDIO, SSE)
102+
- Supports different transport types (STDIO, Remote)
103103
- Provides methods to initialize, check health, and gracefully shut down connections
104104
- Exposes the server's capabilities to the router
105105

106-
### `RouterSseTransport`
106+
### `RouterSseTransport` (deprecated)
107107

108108
Extends the SSE server transport to handle client connections:
109109
- Provides an SSE server endpoint for clients to connect
@@ -130,7 +130,7 @@ Monitors configuration changes:
130130
Defines the configuration for connecting to downstream servers:
131131
- Base `ServerConfig` class with common properties
132132
- `STDIOServerConfig` for command-line based servers
133-
- `SSEServerConfig` for HTTP/SSE based servers
133+
- `RemoteServerConfig` for HTTP/SSE based servers
134134
- Stores necessary connection parameters (command, args, env, URL)
135135

136136
## Namespacing
@@ -148,19 +148,19 @@ This allows the router to route requests to the appropriate server based on the
148148

149149
### Downstream Connections (Router as Client)
150150

151-
1. Router creates persistent connections to downstream MCP servers using STDIO or SSE
151+
1. Router creates persistent connections to downstream MCP servers using STDIO or remote (Streamable HTTP/SSE)
152152
2. Connections are maintained regardless of upstream client presence
153153
3. Server capabilities are fetched and aggregated with namespacing
154154
4. Connections are managed through the `ServerConnection` class
155155
5. Notifications from servers are routed to appropriate upstream clients
156156

157157
### Upstream Connections (Router as Server)
158158

159-
1. Router provides an SSE server interface for upstream clients
159+
1. Router provides an streamable HTTP server interface for upstream clients
160160
2. Clients connect with a profile identifier to determine server visibility
161161
3. Client requests are routed to appropriate downstream servers based on profile
162162
4. Responses and notifications are delivered back to clients
163-
5. Session management is handled by `RouterSseTransport`
163+
5. Session management is handled by `RouterSseTransport`(deprecated)
164164

165165
## Request Routing and Namespacing
166166

@@ -202,7 +202,7 @@ sequenceDiagram
202202
1. **Decoupling**: Upstream clients are decoupled from downstream servers
203203
2. **Resilience**: Client disconnections don't affect server connections
204204
3. **Aggregation**: Multiple capabilities from different servers appear as one
205-
4. **Flexibility**: Supports different transport protocols (STDIO, SSE)
205+
4. **Flexibility**: Supports different transport protocols (STDIO, Remote)
206206
5. **Scalability**: Can manage multiple clients and servers simultaneously
207207
6. **Profile-based Access**: Controls which servers are available to which clients
208208
7. **Dynamic Configuration**: Supports runtime changes to server configurations

pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ name = "mcpm"
77
dynamic = ["version"]
88
description = "MCPM - Model Context Protocol Manager"
99
readme = "README.md"
10-
requires-python = ">=3.10"
10+
requires-python = ">=3.11"
1111
license = "MIT"
1212
authors = [{ name = "MCPM Contributors" }]
1313
maintainers = [{ name = "Path Integral Institute" }]
@@ -30,6 +30,7 @@ dependencies = [
3030
"duckdb>=1.2.2",
3131
"psutil>=7.0.0",
3232
"prompt-toolkit>=3.0.0",
33+
"deprecated>=1.2.18",
3334
]
3435

3536
[project.urls]

src/mcpm/clients/managers/claude_desktop.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from typing import Any, Dict
88

99
from mcpm.clients.base import JSONClientManager
10-
from mcpm.core.schema import ServerConfig, SSEServerConfig
10+
from mcpm.core.schema import RemoteServerConfig, ServerConfig
1111
from mcpm.utils.router_server import format_server_url_with_proxy_headers
1212

1313
logger = logging.getLogger(__name__)
@@ -117,7 +117,7 @@ def _format_router_server(self, profile_name, base_url) -> ServerConfig:
117117
return format_server_url_with_proxy_headers(self.client_key, profile_name, base_url)
118118

119119
def to_client_format(self, server_config: ServerConfig) -> Dict[str, Any]:
120-
if isinstance(server_config, SSEServerConfig):
120+
if isinstance(server_config, RemoteServerConfig):
121121
# use mcp proxy to convert to stdio as sse is not supported for claude desktop yet
122122
return self.to_client_format(server_config.to_mcp_proxy_stdio())
123123
return super().to_client_format(server_config)

src/mcpm/commands/router.py

Lines changed: 37 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717

1818
from mcpm.clients.client_registry import ClientRegistry
1919
from mcpm.router.share import Tunnel
20-
from mcpm.utils.config import ConfigManager
20+
from mcpm.utils.config import MCPM_AUTH_HEADER, MCPM_PROFILE_HEADER, ConfigManager
2121
from mcpm.utils.platform import get_log_directory, get_pid_directory
2222

2323
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s")
@@ -116,8 +116,9 @@ def router():
116116

117117
@router.command(name="on")
118118
@click.help_option("-h", "--help")
119+
@click.option("--sse", "-s", is_flag=True, help="Use SSE endpoint(deprecated)")
119120
@click.option("--verbose", "-v", is_flag=True, help="Enable verbose output")
120-
def start_router(verbose):
121+
def start_router(sse, verbose):
121122
"""Start MCPRouter as a daemon process.
122123
123124
Example:
@@ -140,12 +141,17 @@ def start_router(verbose):
140141
auth_enabled = config.get("auth_enabled", False)
141142
api_key = config.get("api_key")
142143

144+
if sse:
145+
app_path = "mcpm.router.sse_app:app"
146+
else:
147+
app_path = "mcpm.router.app:app"
148+
143149
# prepare uvicorn command
144150
uvicorn_cmd = [
145151
sys.executable,
146152
"-m",
147153
"uvicorn",
148-
"mcpm.router.app:app",
154+
app_path,
149155
"--host",
150156
host,
151157
"--port",
@@ -197,17 +203,29 @@ def start_router(verbose):
197203

198204
api_key = api_key if auth_enabled else None
199205

200-
# Show URL with or without authentication based on API key availability
201-
if api_key:
202-
# Show authenticated URL
203-
console.print(f"SSE Server URL: [green]http://{host}:{port}/sse?s={api_key}[/]")
204-
console.print("\n[bold cyan]To use a specific profile with authentication:[/]")
205-
console.print(f"[green]http://{host}:{port}/sse?s={api_key}&profile=<profile_name>[/]")
206+
if sse:
207+
console.print("\n[bold yellow]SSE router is not recommended[/]")
208+
console.print(f"Remote Server URL: [green]http://{host}:{port}/sse[/]")
209+
if api_key:
210+
console.print("\n[bold cyan]To use a specific profile with authentication:[/]")
211+
console.print(
212+
f"Remote Server URL with authentication: [green]http://{host}:{port}/sse?s={api_key}&profile=<profile_name>[/]"
213+
)
214+
else:
215+
console.print("\n[bold cyan]To use a specific profile:[/]")
216+
console.print(
217+
f"Remote Server URL with authentication: [green]http://{host}:{port}/sse?profile=<profile_name>[/]"
218+
)
206219
else:
207-
# Show URL without authentication
208-
console.print(f"SSE Server URL: [green]http://{host}:{port}/sse[/]")
209-
console.print("\n[bold cyan]To use a specific profile:[/]")
210-
console.print(f"[green]http://{host}:{port}/sse?profile=<profile_name>[/]")
220+
console.print(f"Remote Server URL: [green]http://{host}:{port}/mcp/[/]")
221+
if api_key:
222+
console.print("\n[bold cyan]To use a specific profile with authentication:[/]")
223+
console.print("[bold]Request headers:[/]")
224+
console.print(f"{MCPM_AUTH_HEADER}: {api_key}")
225+
else:
226+
console.print("\n[bold cyan]To use a specific profile:[/]")
227+
console.print("[bold]Request headers:[/]")
228+
console.print(f"{MCPM_PROFILE_HEADER}: <profile_name>")
211229

212230
console.print("\n[yellow]Use 'mcpm router off' to stop the router.[/]")
213231

@@ -432,16 +450,18 @@ def share(address, profile, http):
432450
share_url = tunnel.start_tunnel()
433451
share_pid = tunnel.proc.pid if tunnel.proc else None
434452
api_key = config.get("api_key") if config.get("auth_enabled") else None
435-
share_url = share_url + "/sse"
453+
454+
share_url = share_url + "/mcp/"
436455
# save share pid and link to config
437456
config_manager.save_share_config(share_url, share_pid)
438457
profile = profile or "<your_profile>"
439458

440459
# print share link
441460
console.print(f"[bold green]Router is sharing at {share_url}[/]")
442-
console.print(
443-
f"[green]Your profile can be accessed with the url {share_url}?{f's={api_key}&' if api_key else ''}profile={profile}[/]\n"
444-
)
461+
console.print(f"[green]Your profile can be accessed with the url {share_url}[/]\n")
462+
if api_key:
463+
console.print(f"[green]Authorize with header {MCPM_AUTH_HEADER}: {api_key}[/]")
464+
console.print(f"[green]Specify profile with header {MCPM_PROFILE_HEADER}: {profile}[/]")
445465
console.print(
446466
"[bold yellow]Be careful about the share link, it will be exposed to the public. Make sure to share to trusted users only.[/]"
447467
)

src/mcpm/commands/target.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -56,10 +56,6 @@ def set_target(target):
5656
console.print(f"[bold green]Success:[/] Active client set to {scope}")
5757
else:
5858
# Set the active profile
59-
active_profile = ClientRegistry.get_active_target()
60-
if active_profile:
61-
console.print(f"[bold green]Success:[/] Active profile set to {active_profile}")
62-
6359
profiles = ProfileConfigManager().list_profiles()
6460
if scope not in profiles:
6561
console.print(f"[bold red]Error:[/] Unknown profile: {scope}")

src/mcpm/commands/target_operations/custom.py

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from rich.prompt import Confirm, Prompt
66

77
from mcpm.commands.target_operations.common import client_add_server, determine_scope, profile_add_server
8-
from mcpm.core.schema import SSEServerConfig, STDIOServerConfig
8+
from mcpm.core.schema import RemoteServerConfig, STDIOServerConfig
99
from mcpm.utils.display import print_server_config
1010
from mcpm.utils.scope import ScopeType
1111

@@ -83,12 +83,12 @@ def stdio(server_name, command, args, env, target, force):
8383
@click.option("--target", "-t", help="Target to import server to")
8484
@click.option("--force", is_flag=True, help="Force reinstall if server is already installed")
8585
@click.help_option("-h", "--help")
86-
def sse(server_name, url, header, target, force):
86+
def remote(server_name, url, header, target, force):
8787
"""Add a server by specifying a URL and headers.
8888
Examples:
8989
9090
\b
91-
mcpm import sse <server_name> --url <url> --header <key1>=<value1> --header <key2>=<value2>
91+
mcpm import remote <server_name> --url <url> --header <key1>=<value1> --header <key2>=<value2>
9292
"""
9393
scope_type, scope = determine_scope(target)
9494
if not scope:
@@ -103,7 +103,7 @@ def sse(server_name, url, header, target, force):
103103
console.print(f"[yellow]Ignoring invalid header: {item}[/]")
104104

105105
try:
106-
server_config = SSEServerConfig(
106+
server_config = RemoteServerConfig(
107107
name=server_name,
108108
url=url,
109109
headers=headers,
@@ -123,9 +123,9 @@ def sse(server_name, url, header, target, force):
123123
success = profile_add_server(scope, server_config, force)
124124

125125
if success:
126-
console.print(f"[bold green]SSE server '{server_name}' added successfully to {scope_type} {scope}.")
126+
console.print(f"[bold green]Remote server '{server_name}' added successfully to {scope_type} {scope}.")
127127
else:
128-
console.print(f"[bold red]Failed to add SSE server '{server_name}' to {scope_type} {scope}.")
128+
console.print(f"[bold red]Failed to add remote server '{server_name}' to {scope_type} {scope}.")
129129

130130

131131
@import_server.command()
@@ -142,7 +142,7 @@ def interact(target: str | None = None):
142142
console.print("[red]Server name cannot be empty.[/]")
143143
return
144144

145-
config_type = Prompt.ask("Select server type", choices=["stdio", "sse"], default="stdio")
145+
config_type = Prompt.ask("Select server type", choices=["stdio", "remote"], default="stdio")
146146

147147
if config_type == "stdio":
148148
command = Prompt.ask("Enter command (executable)")
@@ -166,8 +166,8 @@ def interact(target: str | None = None):
166166
except ValueError as e:
167167
console.print(f"[bold red]Error:[/] {e}")
168168
return
169-
elif config_type == "sse":
170-
url = Prompt.ask("Enter SSE server URL")
169+
elif config_type == "remote":
170+
url = Prompt.ask("Enter remote server URL")
171171
headers_input = Prompt.ask("Enter HTTP headers (format: KEY=VAL, comma-separated, optional)", default="")
172172
headers = {}
173173
if headers_input.strip():
@@ -176,7 +176,7 @@ def interact(target: str | None = None):
176176
k, v = pair.split("=", 1)
177177
headers[k.strip()] = v.strip()
178178
try:
179-
server_config = SSEServerConfig(
179+
server_config = RemoteServerConfig(
180180
name=server_name,
181181
url=url,
182182
headers=headers,

src/mcpm/core/schema.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ def get_filtered_env_vars(self, env: Dict[str, str]) -> Dict[str, str]:
5353
return filtered_env
5454

5555

56-
class SSEServerConfig(BaseServerConfig):
56+
class RemoteServerConfig(BaseServerConfig):
5757
url: str
5858
headers: Dict[str, Any] = {}
5959

@@ -75,7 +75,7 @@ def to_mcp_proxy_stdio(self) -> STDIOServerConfig:
7575
)
7676

7777

78-
ServerConfig = Union[STDIOServerConfig, SSEServerConfig]
78+
ServerConfig = Union[STDIOServerConfig, RemoteServerConfig]
7979

8080

8181
class Profile(BaseModel):

0 commit comments

Comments
 (0)