From b37686cf153b984ad73f9c50267b91a8d31e37f4 Mon Sep 17 00:00:00 2001 From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com> Date: Fri, 20 Jun 2025 12:52:42 -0400 Subject: [PATCH] Use trailing slashes --- src/fastmcp/server/http.py | 63 +++++++++----------------------------- src/fastmcp/settings.py | 4 +-- 2 files changed, 16 insertions(+), 51 deletions(-) diff --git a/src/fastmcp/server/http.py b/src/fastmcp/server/http.py index 696f861fd..a80b2235c 100644 --- a/src/fastmcp/server/http.py +++ b/src/fastmcp/server/http.py @@ -310,28 +310,12 @@ def create_streamable_http_app( # Re-raise other RuntimeErrors if they don't match the specific message raise - # Create an ASGI app wrapper that normalizes paths and handles both /mcp and /mcp/ - class PathNormalizingASGIApp: - def __init__(self, app): - self.app = app - - async def __call__(self, scope, receive, send): - # Normalize path to remove trailing slash for MCP SDK - if scope["type"] == "http": - path = scope["path"] - if path.endswith("/") and len(path) > 1: - scope = dict(scope) - scope["path"] = path.rstrip("/") - - await self.app(scope, receive, send) + # Ensure the streamable_http_path ends with a trailing slash to avoid automatic redirects + # when mounting the application + if not streamable_http_path.endswith("/"): + streamable_http_path = streamable_http_path + "/" - # Create the path-normalizing wrapper - normalized_handler = PathNormalizingASGIApp(handle_streamable_http) - - # Create path pattern without trailing slash - path_pattern = streamable_http_path.rstrip("/") - - # Add StreamableHTTP routes with or without auth - add both with and without trailing slash + # Add StreamableHTTP routes with or without auth if auth: auth_middleware, auth_routes, required_scopes = ( setup_auth_middleware_and_routes(auth) @@ -340,38 +324,19 @@ def create_streamable_http_app( server_routes.extend(auth_routes) server_middleware.extend(auth_middleware) - # Auth is enabled, wrap app with RequireAuthMiddleware - wrapped_app = RequireAuthMiddleware(normalized_handler, required_scopes) - - # Add routes for both with and without trailing slash + # Auth is enabled, wrap endpoint with RequireAuthMiddleware server_routes.append( - Route( - path_pattern, - endpoint=wrapped_app, - methods=["GET", "POST"] - ) - ) - server_routes.append( - Route( - path_pattern + "/", - endpoint=wrapped_app, - methods=["GET", "POST"] + Mount( + streamable_http_path, + app=RequireAuthMiddleware(handle_streamable_http, required_scopes), ) ) else: - # No auth required - add routes for both with and without trailing slash + # No auth required server_routes.append( - Route( - path_pattern, - endpoint=normalized_handler, - methods=["GET", "POST"] - ) - ) - server_routes.append( - Route( - path_pattern + "/", - endpoint=normalized_handler, - methods=["GET", "POST"] + Mount( + streamable_http_path, + app=handle_streamable_http, ) ) @@ -400,6 +365,6 @@ def create_streamable_http_app( # Store the FastMCP server instance on the Starlette app state app.state.fastmcp_server = server - app.state.path = streamable_http_path.rstrip("/") + app.state.path = streamable_http_path return app diff --git a/src/fastmcp/settings.py b/src/fastmcp/settings.py index 96fd657b3..cd082166b 100644 --- a/src/fastmcp/settings.py +++ b/src/fastmcp/settings.py @@ -192,9 +192,9 @@ class Settings(BaseSettings): # HTTP settings host: str = "127.0.0.1" port: int = 8000 - sse_path: str = "/sse" + sse_path: str = "/sse/" message_path: str = "/messages/" - streamable_http_path: str = "/mcp" + streamable_http_path: str = "/mcp/" debug: bool = False # error handling