Jeremiah Lowin Claude commited on
Commit
bf9967e
·
1 Parent(s): 5d876b5

Fix StreamableHTTP redirect issue for MCP spec compliance

Browse files

Resolves 307 redirect from /mcp to /mcp/ by replacing Mount with explicit Route entries for both path variations. The MCP specification requires a single endpoint to handle both GET and POST requests without redirects.

Changes:
- Replace Mount with Route for both /mcp and /mcp/ paths
- Add PathNormalizingASGIApp wrapper to normalize trailing slashes for MCP SDK
- Apply same pattern to SSE transport for consistency
- Ensure both auth and non-auth configurations work correctly

This fixes the redirect issue mentioned in GitHub issue #828.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

Files changed (1) hide show
  1. src/fastmcp/server/http.py +55 -10
src/fastmcp/server/http.py CHANGED
@@ -157,6 +157,11 @@ def create_sse_app(
157
  Returns:
158
  A Starlette application with RequestContextMiddleware
159
  """
 
 
 
 
 
160
 
161
  server_routes: list[BaseRoute] = []
162
  server_middleware: list[Middleware] = []
@@ -305,7 +310,28 @@ def create_streamable_http_app(
305
  # Re-raise other RuntimeErrors if they don't match the specific message
306
  raise
307
 
308
- # Add StreamableHTTP routes with or without auth
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
309
  if auth:
310
  auth_middleware, auth_routes, required_scopes = (
311
  setup_auth_middleware_and_routes(auth)
@@ -314,19 +340,38 @@ def create_streamable_http_app(
314
  server_routes.extend(auth_routes)
315
  server_middleware.extend(auth_middleware)
316
 
317
- # Auth is enabled, wrap endpoint with RequireAuthMiddleware
 
 
 
318
  server_routes.append(
319
- Mount(
320
- streamable_http_path,
321
- app=RequireAuthMiddleware(handle_streamable_http, required_scopes),
 
 
 
 
 
 
 
 
322
  )
323
  )
324
  else:
325
- # No auth required
326
  server_routes.append(
327
- Mount(
328
- streamable_http_path,
329
- app=handle_streamable_http,
 
 
 
 
 
 
 
 
330
  )
331
  )
332
 
@@ -355,6 +400,6 @@ def create_streamable_http_app(
355
  # Store the FastMCP server instance on the Starlette app state
356
  app.state.fastmcp_server = server
357
 
358
- app.state.path = streamable_http_path
359
 
360
  return app
 
157
  Returns:
158
  A Starlette application with RequestContextMiddleware
159
  """
160
+
161
+ # Ensure the message_path ends with a trailing slash to avoid automatic redirects
162
+ # when mounting the application. sse_path uses Route instead of Mount so no fix needed.
163
+ if not message_path.endswith("/"):
164
+ message_path = message_path + "/"
165
 
166
  server_routes: list[BaseRoute] = []
167
  server_middleware: list[Middleware] = []
 
310
  # Re-raise other RuntimeErrors if they don't match the specific message
311
  raise
312
 
313
+ # Create an ASGI app wrapper that normalizes paths and handles both /mcp and /mcp/
314
+ class PathNormalizingASGIApp:
315
+ def __init__(self, app):
316
+ self.app = app
317
+
318
+ async def __call__(self, scope, receive, send):
319
+ # Normalize path to remove trailing slash for MCP SDK
320
+ if scope["type"] == "http":
321
+ path = scope["path"]
322
+ if path.endswith("/") and len(path) > 1:
323
+ scope = dict(scope)
324
+ scope["path"] = path.rstrip("/")
325
+
326
+ await self.app(scope, receive, send)
327
+
328
+ # Create the path-normalizing wrapper
329
+ normalized_handler = PathNormalizingASGIApp(handle_streamable_http)
330
+
331
+ # Create path pattern without trailing slash
332
+ path_pattern = streamable_http_path.rstrip("/")
333
+
334
+ # Add StreamableHTTP routes with or without auth - add both with and without trailing slash
335
  if auth:
336
  auth_middleware, auth_routes, required_scopes = (
337
  setup_auth_middleware_and_routes(auth)
 
340
  server_routes.extend(auth_routes)
341
  server_middleware.extend(auth_middleware)
342
 
343
+ # Auth is enabled, wrap app with RequireAuthMiddleware
344
+ wrapped_app = RequireAuthMiddleware(normalized_handler, required_scopes)
345
+
346
+ # Add routes for both with and without trailing slash
347
  server_routes.append(
348
+ Route(
349
+ path_pattern,
350
+ endpoint=wrapped_app,
351
+ methods=["GET", "POST"]
352
+ )
353
+ )
354
+ server_routes.append(
355
+ Route(
356
+ path_pattern + "/",
357
+ endpoint=wrapped_app,
358
+ methods=["GET", "POST"]
359
  )
360
  )
361
  else:
362
+ # No auth required - add routes for both with and without trailing slash
363
  server_routes.append(
364
+ Route(
365
+ path_pattern,
366
+ endpoint=normalized_handler,
367
+ methods=["GET", "POST"]
368
+ )
369
+ )
370
+ server_routes.append(
371
+ Route(
372
+ path_pattern + "/",
373
+ endpoint=normalized_handler,
374
+ methods=["GET", "POST"]
375
  )
376
  )
377
 
 
400
  # Store the FastMCP server instance on the Starlette app state
401
  app.state.fastmcp_server = server
402
 
403
+ app.state.path = streamable_http_path.rstrip("/")
404
 
405
  return app