[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/list walk.
- 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
endpoint event without waiting for an initialize acknowledgment 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
FastMCP and serve over sse on 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/call right after the SSE endpoint event.
- 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_init
This 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) and fastmcp 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
[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/listortools/callrequest before theinitialize→initializedhandshake has been internally marked as complete, causingmcp/server/session.py:193to raiseRuntimeError("Received request before initialization was complete"). The client surfaces this asMCP error -32602 Invalid request parameters.In production this is triggered when:
tools/listwalk.endpointevent without waiting for aninitializeacknowledgment from the server, exposing the race window.Environment
1.27.0and1.26.0(reproduced on both)2.14.5and3.2.4(reproduced on both)sse(uvicorn + starlette)Reproduction
FastMCPand serve oversseon a publicly reachable port.tools/callright after the SSEendpointevent.Root cause
mcp/server/session.py:193(master / 1.27.x):The check itself follows the MCP spec, but the server-side state machine flips to
Initializedonly after thenotifications/initializednotification is received (line ~205 of the same file). With a heavy tool catalog the initialtools/listresponse can be delayed long enough that another inbound request arrives first.Additionally, the SDK does not queue or hold inbound requests during the
Initializingstate — 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
Initializingstate, then process or reject with a clearer error code.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
endpointeventAllow the SSE
endpointevent to carry aninit_required: true|falsehint so well-behaved clients can synchronize without guessing.Workaround currently deployed
Monkeypatch on server startup (
_init_patch.py):This is not spec-compliant (any client can now skip init), but it resolved -32602 for our deployment.
Related logs (sanitized)
Affected projects
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