Skip to content

Commit f447d3b

Browse files
Add support for custom API key and router configuration in MCPRouter
1 parent 674e22f commit f447d3b

File tree

5 files changed

+158
-4
lines changed

5 files changed

+158
-4
lines changed

README.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,30 @@ mcpm router share # Share the router to public
162162
mcpm router unshare # Unshare the router
163163
```
164164

165+
#### Programmatic Usage of MCPRouter
166+
167+
You can also use the `MCPRouter` class programmatically in your Python applications. This is especially useful if you want to integrate MCPM into your own applications or scripts without relying on the global configuration.
168+
169+
```python
170+
from mcpm.router.router import MCPRouter
171+
172+
# Initialize with a custom API key
173+
router = MCPRouter(api_key="your-custom-api-key")
174+
175+
# Initialize with custom router configuration
176+
router_config = {
177+
"host": "localhost",
178+
"port": 8080,
179+
"share_address": "custom.share.address:8080"
180+
}
181+
router = MCPRouter(api_key="your-custom-api-key", router_config=router_config)
182+
183+
# Optionally, create a global config from the router's configuration
184+
router.create_global_config()
185+
```
186+
187+
See the [examples directory](examples/) for more detailed examples of programmatic usage.
188+
165189
### 🛠️ Utilities (`util`)
166190

167191
```bash

examples/custom_api_key_example.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
#!/usr/bin/env python
2+
"""
3+
Example script demonstrating how to use MCPRouter with a custom API key.
4+
"""
5+
6+
import asyncio
7+
from mcpm.router.router import MCPRouter
8+
9+
async def main():
10+
# Initialize the router with a custom API key
11+
router = MCPRouter(api_key="your-custom-api-key")
12+
13+
# Optionally, create a global config from the router's configuration
14+
# This will save the API key to the global config file
15+
# router.create_global_config()
16+
17+
# Initialize the router and start the server
18+
app = await router.get_sse_server_app(allow_origins=["*"])
19+
20+
# Print a message to indicate that the router is ready
21+
print("Router initialized with custom API key")
22+
print("You can now use this router without setting up a global config")
23+
24+
# In a real application, you would start the server here:
25+
# await router.start_sse_server(host="localhost", port=8080, allow_origins=["*"])
26+
27+
if __name__ == "__main__":
28+
asyncio.run(main())
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
#!/usr/bin/env python
2+
"""
3+
Example script demonstrating how to use MCPRouter with custom configuration.
4+
"""
5+
6+
import asyncio
7+
from mcpm.router.router import MCPRouter
8+
9+
async def main():
10+
# Define custom router configuration
11+
router_config = {
12+
"host": "localhost",
13+
"port": 8080,
14+
"share_address": "custom.share.address:8080"
15+
}
16+
17+
# Initialize the router with a custom API key and router configuration
18+
router = MCPRouter(
19+
api_key="your-custom-api-key",
20+
router_config=router_config,
21+
# You can also specify other parameters:
22+
# reload_server=True, # Reload the server when the config changes
23+
# profile_path="/custom/path/to/profile.json", # Custom profile path
24+
# strict=True, # Use strict mode for duplicated tool names
25+
)
26+
27+
# Optionally, create a global config from the router's configuration
28+
# This will save both the API key and router configuration to the global config file
29+
# router.create_global_config()
30+
31+
# Initialize the router and start the server
32+
app = await router.get_sse_server_app(allow_origins=["*"])
33+
34+
# Print a message to indicate that the router is ready
35+
print("Router initialized with custom configuration")
36+
print("You can now use this router without setting up a global config")
37+
38+
# In a real application, you would start the server here:
39+
# await router.start_sse_server(
40+
# host=router_config["host"],
41+
# port=router_config["port"],
42+
# allow_origins=["*"]
43+
# )
44+
45+
if __name__ == "__main__":
46+
asyncio.run(main())

src/mcpm/router/router.py

Lines changed: 53 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,10 @@
2323
from mcpm.monitor.event import trace_event
2424
from mcpm.profile.profile_config import ProfileConfigManager
2525
from mcpm.schemas.server_config import ServerConfig
26-
from mcpm.utils.config import PROMPT_SPLITOR, RESOURCE_SPLITOR, RESOURCE_TEMPLATE_SPLITOR, TOOL_SPLITOR
26+
from mcpm.utils.config import (
27+
PROMPT_SPLITOR, RESOURCE_SPLITOR, RESOURCE_TEMPLATE_SPLITOR, TOOL_SPLITOR,
28+
ConfigManager, DEFAULT_HOST, DEFAULT_PORT, DEFAULT_SHARE_ADDRESS
29+
)
2730

2831
from .client_connection import ServerConnection
2932
from .transport import RouterSseTransport
@@ -36,16 +39,42 @@ class MCPRouter:
3639
"""
3740
A router that aggregates multiple MCP servers (SSE/STDIO) and
3841
exposes them as a single SSE server.
42+
43+
Example:
44+
```python
45+
# Initialize with a custom API key
46+
router = MCPRouter(api_key="your-api-key")
47+
48+
# Initialize with custom router configuration
49+
router_config = {
50+
"host": "localhost",
51+
"port": 8080,
52+
"share_address": "custom.share.address:8080"
53+
}
54+
router = MCPRouter(api_key="your-api-key", router_config=router_config)
55+
56+
# Create a global config from the router's configuration
57+
router.create_global_config()
58+
```
3959
"""
4060

41-
def __init__(self, reload_server: bool = False, profile_path: str | None = None, strict: bool = False) -> None:
61+
def __init__(
62+
self,
63+
reload_server: bool = False,
64+
profile_path: str | None = None,
65+
strict: bool = False,
66+
api_key: str | None = None,
67+
router_config: dict | None = None
68+
) -> None:
4269
"""
4370
Initialize the router.
4471
4572
:param reload_server: Whether to reload the server when the config changes
4673
:param profile_path: Path to the profile file
4774
:param strict: Whether to use strict mode for duplicated tool name.
4875
If True, raise error when duplicated tool name is found else auto resolve by adding server name prefix
76+
:param api_key: Optional API key to use for authentication
77+
:param router_config: Optional router configuration to use instead of the global config
4978
"""
5079
self.server_sessions: t.Dict[str, ServerConnection] = {}
5180
self.capabilities_mapping: t.Dict[str, t.Dict[str, t.Any]] = defaultdict(dict)
@@ -60,6 +89,26 @@ def __init__(self, reload_server: bool = False, profile_path: str | None = None,
6089
if reload_server:
6190
self.watcher = ConfigWatcher(self.profile_manager.profile_path)
6291
self.strict: bool = strict
92+
self.api_key = api_key
93+
self.router_config = router_config
94+
95+
def create_global_config(self) -> None:
96+
"""
97+
Create a global configuration from the router's configuration.
98+
This is useful if you want to initialize the router with a config
99+
but also want that config to be available globally.
100+
"""
101+
if self.api_key is not None:
102+
config_manager = ConfigManager()
103+
# Save the API key to the global config
104+
config_manager.save_share_config(api_key=self.api_key)
105+
106+
# If router_config is provided, save it to the global config
107+
if self.router_config is not None:
108+
host = self.router_config.get("host", DEFAULT_HOST)
109+
port = self.router_config.get("port", DEFAULT_PORT)
110+
share_address = self.router_config.get("share_address", DEFAULT_SHARE_ADDRESS)
111+
config_manager.save_router_config(host, port, share_address)
63112

64113
def get_unique_servers(self) -> list[ServerConfig]:
65114
profiles = self.profile_manager.list_profiles()
@@ -496,7 +545,8 @@ async def get_sse_server_app(
496545
"""
497546
await self.initialize_router()
498547

499-
sse = RouterSseTransport("/messages/")
548+
# Pass the API key to the RouterSseTransport
549+
sse = RouterSseTransport("/messages/", api_key=self.api_key)
500550

501551
async def handle_sse(request: Request) -> None:
502552
async with sse.connect_sse(

src/mcpm/router/transport.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,8 +66,9 @@ def get_key_from_scope(scope: Scope, key_name: str) -> str | None:
6666
class RouterSseTransport(SseServerTransport):
6767
"""A SSE server transport that is used by the router to handle client connections."""
6868

69-
def __init__(self, *args, **kwargs):
69+
def __init__(self, *args, api_key: str | None = None, **kwargs):
7070
self._session_id_to_identifier: dict[UUID, ClientIdentifier] = {}
71+
self.api_key = api_key
7172
super().__init__(*args, **kwargs)
7273

7374
@asynccontextmanager
@@ -238,6 +239,11 @@ async def handle_post_message(self, scope: Scope, receive: Receive, send: Send):
238239
self._session_id_to_identifier.pop(session_id, None)
239240

240241
def _validate_api_key(self, scope: Scope, api_key: str | None) -> bool:
242+
# If we have a directly provided API key and it matches the request's API key, return True
243+
if self.api_key is not None and api_key == self.api_key:
244+
return True
245+
246+
# Otherwise, fall back to the original validation logic
241247
try:
242248
config_manager = ConfigManager()
243249
host = get_key_from_scope(scope, key_name="host") or ""

0 commit comments

Comments
 (0)