-
Notifications
You must be signed in to change notification settings - Fork 2.9k
Description
Initial Checks
- I confirm that I'm using the latest version of MCP Python SDK
- I confirm that I searched for my issue in https://github.com/modelcontextprotocol/python-sdk/issues before opening this issue
Description
Bug Description
SSE connections hang indefinitely when using StreamableHTTPServerTransport in stateless mode with responses containing 3+ items. The issue is caused by zero-buffer memory streams that block send() until receive() is called, creating a race condition between the response writer and the SSE stream iterator.
Related issues: #262 describes similar symptoms (client hangs on call_tool()) but root cause wasn't identified. This issue provides the specific cause and fix.
Expected Behavior
All tool responses should complete regardless of response size.
Actual Behavior
- 1-2 items: Response returns immediately (~150ms)
- 3+ items: Request hangs indefinitely (deadlock)
Root Cause Analysis
The issue is in mcp/server/streamable_http.py:
Line 412 - zero-buffer request stream
self._request_streams[request_id] = anyio.create_memory_object_streamEventMessage
Line 460 - zero-buffer SSE stream
sse_stream_writer, sse_stream_reader = anyio.create_memory_object_streamdict[str, str]
Race condition flow:
- tg.start_soon(response, ...) starts SSE response task (non-blocking)
- await writer.send(session_message) sends request to MCP server
- MCP server processes quickly and calls message_router
- message_router tries await request_streams[id][0].send(EventMessage(...))
- DEADLOCK: If SSE writer hasn't started iterating yet, send() blocks forever
With zero-buffer streams, send() blocks until the receiver calls receive(). When the MCP server processes faster than the SSE writer task starts, deadlock occurs.
Why timing matters:
- Small responses (1-2 items): SSE writer task starts before MCP response arrives → works
- Larger responses (3+ items): MCP processes faster → response arrives before SSE iterator starts → blocked forever
Proposed Fix
Increase buffer size from 0 to a reasonable value (e.g., 10 or 100):
Line 412
self._request_streams[request_id] = anyio.create_memory_object_streamEventMessage
Line 460
sse_stream_writer, sse_stream_reader = anyio.create_memory_object_streamdict[str, str]
Alternative fix: Use await tg.start() instead of tg.start_soon() to ensure SSE writer is ready before sending requests (requires EventSourceResponse to support task status protocol).
Workaround
We've applied this fix via sed patch in our Dockerfile:
RUN sed -i 's/create_memory_object_stream[EventMessage](0)/create_memory_object_streamEventMessage/g'
/usr/local/lib/python3.11/site-packages/mcp/server/streamable_http.py &&
sed -i 's/create_memory_object_stream[dict[str, str]](0)/create_memory_object_streamdict[str, str]/g'
/usr/local/lib/python3.11/site-packages/mcp/server/streamable_http.py
This resolves the issue in our production environment.
Example Code
from fastmcp import FastMCP
import json
mcp = FastMCP("test-server")
@mcp.tool()
async def test_tool() -> str:
# Returns JSON with 3+ items - will hang
return json.dumps({
"results": [{"n": "a"}, {"n": "b"}, {"n": "c"}]
})
app = mcp.http_app(path="/mcp", stateless_http=True)
1. Call the tool via HTTP POST to /mcp
2. Response hangs indefinitely for tools returning 3+ items in arrays
3. Tools returning 1-2 items work correctlyPython & MCP Python SDK
- MCP SDK version: 1.23.3
- Python version: 3.11
- FastMCP version: 2.13.1
- Transport: StreamableHTTP with stateless_http=True