diff --git a/litellm/proxy/proxy_server.py b/litellm/proxy/proxy_server.py index 9c29927c5cb..7f8b94d0b8f 100644 --- a/litellm/proxy/proxy_server.py +++ b/litellm/proxy/proxy_server.py @@ -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)