Skip to content

Race condition between SSE session init and tools/call rejection (-32602) #2583

@hyoseop1231

Description

@hyoseop1231

[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 initializeinitialized 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

  1. Build an MCP server that registers >100 tools using FastMCP and serve over sse on a publicly reachable port.
  2. 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).
  3. Have Client B issue any tools/call right after the SSE endpoint event.
  4. 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
    
  5. 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions