Add mount_path support for proper SSE endpoint routing with multiple FastMCP servers (#540)

Co-authored-by: ihrpr <inna.hrpr@gmail.com>
This commit is contained in:
tim-watcha
2025-05-07 19:14:25 +09:00
committed by GitHub
parent 3b1b213a96
commit e0d443c95e
3 changed files with 178 additions and 8 deletions

View File

@@ -88,6 +88,7 @@ class Settings(BaseSettings, Generic[LifespanResultT]):
# HTTP settings
host: str = "0.0.0.0"
port: int = 8000
mount_path: str = "/" # Mount path (e.g. "/github", defaults to root path)
sse_path: str = "/sse"
message_path: str = "/messages/"
@@ -184,11 +185,16 @@ class FastMCP:
def instructions(self) -> str | None:
return self._mcp_server.instructions
def run(self, transport: Literal["stdio", "sse"] = "stdio") -> None:
def run(
self,
transport: Literal["stdio", "sse"] = "stdio",
mount_path: str | None = None,
) -> None:
"""Run the FastMCP server. Note this is a synchronous function.
Args:
transport: Transport protocol to use ("stdio" or "sse")
mount_path: Optional mount path for SSE transport
"""
TRANSPORTS = Literal["stdio", "sse"]
if transport not in TRANSPORTS.__args__: # type: ignore
@@ -197,7 +203,7 @@ class FastMCP:
if transport == "stdio":
anyio.run(self.run_stdio_async)
else: # transport == "sse"
anyio.run(self.run_sse_async)
anyio.run(lambda: self.run_sse_async(mount_path))
def _setup_handlers(self) -> None:
"""Set up core MCP protocol handlers."""
@@ -558,11 +564,11 @@ class FastMCP:
self._mcp_server.create_initialization_options(),
)
async def run_sse_async(self) -> None:
async def run_sse_async(self, mount_path: str | None = None) -> None:
"""Run the server using SSE transport."""
import uvicorn
starlette_app = self.sse_app()
starlette_app = self.sse_app(mount_path)
config = uvicorn.Config(
starlette_app,
@@ -573,7 +579,33 @@ class FastMCP:
server = uvicorn.Server(config)
await server.serve()
def sse_app(self) -> Starlette:
def _normalize_path(self, mount_path: str, endpoint: str) -> str:
"""
Combine mount path and endpoint to return a normalized path.
Args:
mount_path: The mount path (e.g. "/github" or "/")
endpoint: The endpoint path (e.g. "/messages/")
Returns:
Normalized path (e.g. "/github/messages/")
"""
# Special case: root path
if mount_path == "/":
return endpoint
# Remove trailing slash from mount path
if mount_path.endswith("/"):
mount_path = mount_path[:-1]
# Ensure endpoint starts with slash
if not endpoint.startswith("/"):
endpoint = "/" + endpoint
# Combine paths
return mount_path + endpoint
def sse_app(self, mount_path: str | None = None) -> Starlette:
"""Return an instance of the SSE server app."""
message_dispatch = self.settings.message_dispatch
if message_dispatch is None:
@@ -585,10 +617,20 @@ class FastMCP:
from starlette.middleware import Middleware
from starlette.routing import Mount, Route
# Update mount_path in settings if provided
if mount_path is not None:
self.settings.mount_path = mount_path
# Create normalized endpoint considering the mount path
normalized_message_endpoint = self._normalize_path(
self.settings.mount_path, self.settings.message_path
)
# Set up auth context and dependencies
sse = SseServerTransport(
self.settings.message_path, message_dispatch=message_dispatch
normalized_message_endpoint,
message_dispatch=message_dispatch
)
async def handle_sse(scope: Scope, receive: Receive, send: Send):