Skip to content

Commit 1bd89ff

Browse files
committed
Add mount_path parameter to sse_app method
- Add mount_path parameter to sse_app method for direct path configuration - Update run_sse_async and run methods to support mount_path parameter - Update README.md with examples for different mounting methods - Add tests for new parameter functionality
1 parent d9d6bfe commit 1bd89ff

File tree

3 files changed

+55
-10
lines changed

3 files changed

+55
-10
lines changed

README.md

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -383,7 +383,7 @@ app = Starlette(
383383
app.router.routes.append(Host('mcp.acme.corp', app=mcp.sse_app()))
384384
```
385385

386-
When mounting multiple MCP servers under different paths, you need to configure the mount path for each server:
386+
When mounting multiple MCP servers under different paths, you can configure the mount path in several ways:
387387

388388
```python
389389
from starlette.applications import Starlette
@@ -394,20 +394,31 @@ from mcp.server.fastmcp import FastMCP
394394
github_mcp = FastMCP("GitHub API")
395395
browser_mcp = FastMCP("Browser")
396396
curl_mcp = FastMCP("Curl")
397+
search_mcp = FastMCP("Search")
397398

398-
# Configure mount paths for each server
399+
# Method 1: Configure mount paths via settings (recommended for persistent configuration)
399400
github_mcp.settings.mount_path = "/github"
400401
browser_mcp.settings.mount_path = "/browser"
401-
curl_mcp.settings.mount_path = "/curl"
402+
403+
# Method 2: Pass mount path directly to sse_app (preferred for ad-hoc mounting)
404+
# This approach doesn't modify the server's settings permanently
402405

403406
# Create Starlette app with multiple mounted servers
404407
app = Starlette(
405408
routes=[
409+
# Using settings-based configuration
406410
Mount("/github", app=github_mcp.sse_app()),
407411
Mount("/browser", app=browser_mcp.sse_app()),
408-
Mount("/curl", app=curl_mcp.sse_app()),
412+
413+
# Using direct mount path parameter
414+
Mount("/curl", app=curl_mcp.sse_app("/curl")),
415+
Mount("/search", app=search_mcp.sse_app("/search")),
409416
]
410417
)
418+
419+
# Method 3: For direct execution, you can also pass the mount path to run()
420+
if __name__ == "__main__":
421+
search_mcp.run(transport="sse", mount_path="/search")
411422
```
412423

413424
For more information on mounting applications in Starlette, see the [Starlette documentation](https://www.starlette.io/routing/#submounting-routes).

src/mcp/server/fastmcp/server.py

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -144,11 +144,14 @@ def name(self) -> str:
144144
def instructions(self) -> str | None:
145145
return self._mcp_server.instructions
146146

147-
def run(self, transport: Literal["stdio", "sse"] = "stdio") -> None:
147+
def run(
148+
self, transport: Literal["stdio", "sse"] = "stdio", mount_path: str = ""
149+
) -> None:
148150
"""Run the FastMCP server. Note this is a synchronous function.
149151
150152
Args:
151153
transport: Transport protocol to use ("stdio" or "sse")
154+
mount_path: Optional mount path for SSE transport
152155
"""
153156
TRANSPORTS = Literal["stdio", "sse"]
154157
if transport not in TRANSPORTS.__args__: # type: ignore
@@ -157,7 +160,7 @@ def run(self, transport: Literal["stdio", "sse"] = "stdio") -> None:
157160
if transport == "stdio":
158161
anyio.run(self.run_stdio_async)
159162
else: # transport == "sse"
160-
anyio.run(self.run_sse_async)
163+
anyio.run(lambda: self.run_sse_async(mount_path))
161164

162165
def _setup_handlers(self) -> None:
163166
"""Set up core MCP protocol handlers."""
@@ -463,11 +466,11 @@ async def run_stdio_async(self) -> None:
463466
self._mcp_server.create_initialization_options(),
464467
)
465468

466-
async def run_sse_async(self) -> None:
469+
async def run_sse_async(self, mount_path: str = "") -> None:
467470
"""Run the server using SSE transport."""
468471
import uvicorn
469472

470-
starlette_app = self.sse_app()
473+
starlette_app = self.sse_app(mount_path)
471474

472475
config = uvicorn.Config(
473476
starlette_app,
@@ -504,8 +507,11 @@ def _normalize_path(self, mount_path: str, endpoint: str) -> str:
504507
# Combine paths
505508
return mount_path + endpoint
506509

507-
def sse_app(self) -> Starlette:
510+
def sse_app(self, mount_path: str = "") -> Starlette:
508511
"""Return an instance of the SSE server app."""
512+
# Update mount_path in settings if provided
513+
if mount_path:
514+
self.settings.mount_path = mount_path
509515
# Create normalized endpoint considering the mount path
510516
normalized_endpoint = self._normalize_path(
511517
self.settings.mount_path, self.settings.message_path

tests/server/fastmcp/test_server.py

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ async def test_sse_app_with_mount_path(self):
6565
# Verify _normalize_path was called with correct args
6666
mock_normalize.assert_called_once_with("/", "/messages/")
6767

68-
# Test with custom mount path
68+
# Test with custom mount path in settings
6969
mcp = FastMCP()
7070
mcp.settings.mount_path = "/custom"
7171
with patch.object(
@@ -74,10 +74,20 @@ async def test_sse_app_with_mount_path(self):
7474
mcp.sse_app()
7575
# Verify _normalize_path was called with correct args
7676
mock_normalize.assert_called_once_with("/custom", "/messages/")
77+
78+
# Test with mount_path parameter
79+
mcp = FastMCP()
80+
with patch.object(
81+
mcp, "_normalize_path", return_value="/param/messages/"
82+
) as mock_normalize:
83+
mcp.sse_app(mount_path="/param")
84+
# Verify _normalize_path was called with correct args
85+
mock_normalize.assert_called_once_with("/param", "/messages/")
7786

7887
@pytest.mark.anyio
7988
async def test_starlette_routes_with_mount_path(self):
8089
"""Test that Starlette routes are correctly configured with mount path."""
90+
# Test with mount path in settings
8191
mcp = FastMCP()
8292
mcp.settings.mount_path = "/api"
8393
app = mcp.sse_app()
@@ -95,6 +105,24 @@ async def test_starlette_routes_with_mount_path(self):
95105
assert (
96106
mount_routes[0].path == "/messages"
97107
), "Mount route path should be /messages"
108+
109+
# Test with mount path as parameter
110+
mcp = FastMCP()
111+
app = mcp.sse_app(mount_path="/param")
112+
113+
# Find routes by type
114+
sse_routes = [r for r in app.routes if isinstance(r, Route)]
115+
mount_routes = [r for r in app.routes if isinstance(r, Mount)]
116+
117+
# Verify routes exist
118+
assert len(sse_routes) == 1, "Should have one SSE route"
119+
assert len(mount_routes) == 1, "Should have one mount route"
120+
121+
# Verify path values
122+
assert sse_routes[0].path == "/sse", "SSE route path should be /sse"
123+
assert (
124+
mount_routes[0].path == "/messages"
125+
), "Mount route path should be /messages"
98126

99127
@pytest.mark.anyio
100128
async def test_non_ascii_description(self):

0 commit comments

Comments
 (0)