fix(server): guard GET SSE response with CancelScope to prevent Windows deadlock (#2653)
Summary
Fixes #2653 — mcp.server.fastmcp.FastMCP HTTP transport deadlocks after ~5 sequential client sessions on Windows.
Root Cause
sse-starlette's EventSourceResponse.__call__ creates an internal anyio.TaskGroup whose child tasks (_ping, _listen_for_disconnect, _listen_for_exit_signal, standalone_sse_writer) may not respond to cancellation promptly on Windows ProactorEventLoop. When the client disconnects, orphan children remain parked in anyio.sleep() or Event.wait(), preventing TaskGroup.__aexit__ from completing.
The _handle_get_request handler directly awaits the EventSourceResponse callable, so the hung TaskGroup blocks the handler forever. Each session that opens a GET SSE stream and disconnects leaks one such hung task group. After enough sessions (~5–18 depending on Python version), the next initialize POST never dispatches and the server deadlocks.
The raw mcp.server.lowlevel.Server + StreamableHTTPSessionManager path is unaffected because it writes text/event-stream directly without sse-starlette. The Node.js reference server is also unaffected. The defect is confined to the FastMCP wrapper layer's use of sse-starlette.EventSourceResponse.
Fix
Three changes in src/mcp/server/streamable_http.py:
-
__init__: Add_active_get_response_scope: anyio.CancelScope | Noneattribute to track the in-flight GET SSE response. -
_handle_get_request: Wrap theawait response(...)call with the cancel scope instead of awaiting it directly. This givesterminate()a handle to force-cancel a hung response. -
terminate(): Cancel_active_get_response_scope(if set) before closing streams. This breaks thesse-starletteTaskGroup deadlock and allows the session to clean up, even on Windows ProactorEventLoop.
The fix is minimal (19 insertions, 5 deletions), backward-compatible, and targets only the GET SSE path where EventSourceResponse is awaited directly. The POST handler already runs EventSourceResponse inside an anyio.create_task_group() which provides equivalent cancellation safety.
Related
- PrefectHQ/fastmcp#4192 — full investigation, cross-control matrix, deterministic reproduction
sse-starlette— issue is in the library's TaskGroup cleanup on Windows, but the SDK can defend against it here