Repository navigation
Race condition between SSE session init and tools/call rejection (-32602) #2583
Description
[Issue Draft] MCP SDK: Race condition between SSE session init and tools/call rejection (-32602)
Summary
When a MCP server uses the SSE transport, a new SSE session can receive a tools/list or tools/call request before the initialize → initialized handshake has been internally marked as complete, causing mcp/server/session.py:193 to raise RuntimeError("Received request before initialization was complete"). The client surfaces this as MCP error -32602 Invalid request parameters.
In production this is triggered when:
- The server registers a large number of tools at startup (e.g. 184 or 380 tools), slowing the first
tools/listwalk. - Multiple concurrent SSE clients open new sessions in parallel (in our case: a gateway client + Claude Code MCP client on the same host).
- Some clients send the next request immediately after receiving the
endpointevent without waiting for aninitializeacknowledgment from the server, exposing the race window.
Environment
| Field | Value |
|---|---|
| mcp SDK | 1.27.0 and 1.26.0 (reproduced on both) |
| fastmcp | 2.14.5 and 3.2.4 (reproduced on both) |
| Transport | sse (uvicorn + starlette) |
| Python | 3.12.12 |
| OS | macOS Tahoe 25.2.0 (Darwin arm64) |
| Tools registered | 184 (kitech-mail) / 380 (kitech-eip) |
| Concurrent clients | 2 (hermes gateway + Claude Code MCP) |
Reproduction
- Build an MCP server that registers >100 tools using
FastMCPand serve oversseon a publicly reachable port. - Start two clients in parallel:
- Client A: standard hermes/gateway MCP client (keeps SSE session alive).
- Client B: Claude Code MCP client (creates a new session per call).
- Have Client B issue any
tools/callright after the SSEendpointevent. - Observe in server logs:
WARNING:root:Failed to validate request: Received request before initialization was complete INFO: 127.0.0.1:NNNN - "POST /messages/?session_id=XXXXXXX HTTP/1.1" 202 Accepted - Client B receives:
{"jsonrpc":"2.0","id":N,"error":{"code":-32602,"message":"Invalid request parameters"}}
Root cause
mcp/server/session.py:193 (master / 1.27.x):
case _:
if self._initialization_state != InitializationState.Initialized:
raise RuntimeError("Received request before initialization was complete")The check itself follows the MCP spec, but the server-side state machine flips to Initialized only after the notifications/initialized notification is received (line ~205 of the same file). With a heavy tool catalog the initial tools/list response can be delayed long enough that another inbound request arrives first.
Additionally, the SDK does not queue or hold inbound requests during the Initializing state — they are silently rejected, leaving the client to interpret the -32602 as "invalid params" instead of "init not finished".
Suggested fixes (in order of safety)
1. Backpressure / queue during init
Hold inbound non-ping requests for up to a small timeout while the server is still in the Initializing state, then process or reject with a clearer error code.
case _:
if self._initialization_state != InitializationState.Initialized:
# New: wait briefly for init to complete
try:
await asyncio.wait_for(
self._initialized_event.wait(), timeout=2.0
)
except asyncio.TimeoutError:
raise RuntimeError(
"Request received during initialization; client should send initialize first"
)2. Distinct error code
If still rejecting, return a dedicated error code (e.g. -32002 "not initialized") instead of conflating with -32602 (invalid params). This avoids misleading debugging downstream.
3. Initialization-state hint in endpoint event
Allow the SSE endpoint event to carry an init_required: true|false hint so well-behaved clients can synchronize without guessing.
Workaround currently deployed
Monkeypatch on server startup (_init_patch.py):
import mcp.server.session as _mcp_session
_orig_init = _mcp_session.ServerSession.__init__
def _patched_session_init(self, *args, **kwargs):
_orig_init(self, *args, **kwargs)
self._initialization_state = _mcp_session.InitializationState.Initialized
_mcp_session.ServerSession.__init__ = _patched_session_initThis is not spec-compliant (any client can now skip init), but it resolved -32602 for our deployment.
Related logs (sanitized)
INFO: 127.0.0.1:51436 - "GET /sse HTTP/1.1" 200 OK
INFO: 127.0.0.1:51438 - "POST /messages/?session_id=0ec570280f5447e5985b3ab9dd1aa541 HTTP/1.1" 202 Accepted
INFO: 127.0.0.1:51438 - "POST /messages/?session_id=0ec570280f5447e5985b3ab9dd1aa541 HTTP/1.1" 202 Accepted
INFO: 127.0.0.1:51438 - "POST /messages/?session_id=0ec570280f5447e5985b3ab9dd1aa541 HTTP/1.1" 202 Accepted
INFO: 192.168.0.X:63541 - "POST /messages/?session_id=bc95a80d6b014b03a75b068be6c38211 HTTP/1.1" 202 Accepted
WARNING:root: Failed to validate request: Received request before initialization was complete
Affected projects
- Production deployment with 184 + 380 tools per server (downstream tooling pipeline)
- Race window observed both with
fastmcp 2.14.5(mcp 1.26.0) andfastmcp 3.2.4(mcp 1.27.0)
Happy to PR a fix on top of this report — please advise which of the three suggestions above is preferred direction.
Generated: 2026-05-13 by ultraman-mcp-audit workflow
Activity
maxisbey commentedon May 18, 2026
Closing as a duplicate of #423 — the logs show the client never sent
initializebeforetools/call, which is the same root cause #423 tracks (not a race within the SDK). Feel free to add your logs to the discussion there. If this turns out to be a distinct issue, feel free to reopen or open a new one.AI Disclaimer