Repository navigation
Description
Description
Adding a Starlette BaseHTTPMiddleware (e.g., for auth) to FastMCP.streamable_http_app() silently breaks the MCP server. Every request crashes with ClosedResourceError — the client sees "peer closed connection without sending complete message body."
Reproduction
from mcp.server.fastmcp import FastMCP
from starlette.middleware.base import BaseHTTPMiddleware
mcp = FastMCP("test")
# Any BaseHTTPMiddleware — even a no-op passthrough
class AuthMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request, call_next):
return await call_next(request)
mcp.streamable_http_app().add_middleware(BaseHTTPMiddleware, dispatch=AuthMiddleware)Run the server, send any MCP initialize request → ClosedResourceError.
Root cause
BaseHTTPMiddleware wraps the ASGI receive/send channels in a way incompatible with SSE streaming. This is a known Starlette limitation (Kludex/starlette#919), but FastMCP users hit it naturally when they reach for the obvious auth pattern.
Expected behavior
At minimum: warn at startup when a BaseHTTPMiddleware subclass is detected on a streamable HTTP app. Ideally: document prominently that users should use FastMCP's own Middleware class (fastmcp.server.middleware.Middleware) instead of Starlette's BaseHTTPMiddleware.
Workaround (raw ASGI middleware)
from starlette.types import ASGIApp, Receive, Scope, Send
class RawAuthMiddleware:
def __init__(self, app: ASGIApp):
self.app = app
async def __call__(self, scope: Scope, receive: Receive, send: Send):
# auth check here — read headers from scope
await self.app(scope, receive, send)
mcp.streamable_http_app().add_middleware(RawAuthMiddleware)Environment
- mcp / FastMCP from
modelcontextprotocol/python-sdk - Starlette (any version — this is architectural, not a regression)
- Python 3.12
Activity
jstar0 commentedon May 27, 2026
I can take a focused look at this if maintainers agree the right scope is a warning/docs/test change rather than trying to work around Starlette's
BaseHTTPMiddlewarebehavior inside the transport.The implementation I would aim for is narrow: document the incompatibility for Streamable HTTP apps, point users toward raw ASGI middleware or SDK middleware instead, and add a regression test for the warning/guard path. I will wait for maintainer direction before opening a PR.
norika1207-lab commentedon Jun 2, 2026
maxisbey commentedon Aug 14, 2026
Thanks for the report. I couldn't reproduce this as described: a no-op
BaseHTTPMiddlewaresubclass added tostreamable_http_app()under uvicorn, with a real streamable-HTTP client doinginitialize/list_tools/call_tool, works on both currentmain(2.x) and 1.29.0. Could you share the actual traceback, yourmcpandstarletteversions, and whetherstateless_http,json_response, or a standalone GET stream is involved? (Also note the snippet as posted passes the class itself asdispatch=, which fails on its own — aclass M(BaseHTTPMiddleware)withapp.add_middleware(M)is the shape I tested.) Marking as needs repro for now.AI Disclaimer
HarperZ9 commentedon Aug 22, 2026
Ran the config matrix you asked for, and the result is a bounded non-repro.
Method. A real uvicorn server per config, the SDK's own streamable-HTTP client doing
initialize/list_tools/call_tool, plus a standalone GET listen stream opened with the session id. The middleware is the corrected shape you noted:class NoOpAuthMiddleware(BaseHTTPMiddleware)withapp.add_middleware(NoOpAuthMiddleware). Axes: middleware on/off xstateless_httpxjson_response, so every row withmw 0is a control. "stream open (idle)" means the GET channel returned 200 and stayed open with no events inside an 8s window, which is the healthy outcome for an idle listen channel.Result: 32 of 32 cases pass. Client ops succeed and the GET stream stays healthy in every combination, middleware included, on all four version stacks:
mcp 2.0.0, starlette 1.6.0
mcp 1.29.0, starlette 1.6.0
mcp 1.27.1 (current when this issue was filed), starlette 1.6.0
mcp 1.27.1, starlette 0.27.0 (the oldest version the SDK allows, the starlette#919-era codebase)
Host: Windows 11, Python 3.14.6, uvicorn 0.52.4. Nothing here is platform-specific in principle, and the harness below is two small files if anyone wants the Linux datapoint.
Given the middleware-on and middleware-off rows are indistinguishable on the filing-era SDK and on the oldest supported starlette, this looks consistent with closing unless the reporter can supply the exact
mcp/starletteversions and a traceback. The one failure mode I could not rule out is a different middleware than the no-op shape tested here (one that consumes the request body or replaces the response object would interact differently with streaming).harness: server_app.py + matrix.py
server_app.pymatrix.py