fix(proxy): avoid 307 redirect on POST /mcp (fixes #23688)

Starlette's Router redirects /mcp -> /mcp/ with 307 Temporary Redirect
when the MCP sub-app is mounted at /mcp. Many HTTP clients (and MCP
clients like Claude Code) do not follow redirects for POST requests,
causing the request body to be dropped and the connection to fail.

Add a lightweight pure-ASGI middleware that internally rewrites /mcp to
/mcp/ before routing, so the mounted sub-app handles the request
directly. Using a raw ASGI middleware (instead of BaseHTTPMiddleware)
preserves SSE streaming used by the MCP transport layer.
This commit is contained in:
voidborne-d 2026-03-15 21:11:53 +00:00
parent 548e7ebd60
commit 8a43e213b4

View file

@ -13399,3 +13399,23 @@ async def dynamic_mcp_route(mcp_server_name: str, request: Request):
app.mount(path=BASE_MCP_ROUTE, app=mcp_app)
app.include_router(mcp_rest_endpoints_router)
app.include_router(mcp_discoverable_endpoints_router)
# Rewrite /mcp -> /mcp/ internally to avoid 307 redirect from Starlette's
# mounted sub-application routing. Many MCP clients (e.g. Claude Code) send
# POST /mcp and do not follow redirects for POST, which drops the request body.
# Using a pure ASGI middleware (not BaseHTTPMiddleware) keeps SSE streaming intact.
class _MCPTrailingSlashMiddleware:
"""Transparently append '/' to the MCP mount path so Starlette serves it
directly instead of responding with 307 Temporary Redirect."""
def __init__(self, app: Any) -> None: # noqa: F811
self.app = app
async def __call__(self, scope: Any, receive: Any, send: Any) -> None:
if scope["type"] == "http" and scope.get("path") == BASE_MCP_ROUTE:
scope = dict(scope, path=BASE_MCP_ROUTE + "/")
await self.app(scope, receive, send)
app.add_middleware(_MCPTrailingSlashMiddleware)