Skip to content

Claude Code client sends tools/call without initialize handshake — SSE transport rejects with -32602 #2579

@Aksels73

Description

@Aksels73

Summary

Claude Code MCP clients (the official claude-code CLI / SDK) intermittently send tools/call requests directly after opening an SSE session, without first sending the initialize request and notifications/initialized notification. The server then rejects the request with RuntimeError("Received request before initialization was complete") (mcp/server/session.py:193), which surfaces to the client as -32602: Invalid request parameters.

Likely related to #1844, but we have a concrete reproduction with full server-side debug logs.

Environment

  • mcp 1.27.0 (also reproduces with 1.14.0, 1.27.1)
  • Python 3.13 on Debian 14
  • Transport: SSE via SseServerTransport + Starlette/Uvicorn
  • Client: claude-code CLI (current version, agent-SDK based)
  • Reproduces with multiple parallel SSE sessions from the same host (typical of a multi-agent setup)

Server-side debug log (raw)

[INFO]  openproject-mcp-sse - POST /messages session=f3ee2d98… method=tools/call id=14 body={"method": "tools/call", "params": {"name": "test_connection", "arguments": {}, "_meta": {"claudecode/toolUseId": "toolu_01SqXkUVGvojypc6RaA4UriG", "progressToken": 14}}, "jsonrpc": "2.0", "id": 14}
[DEBUG] mcp.server.sse - Validated client message: root=JSONRPCRequest(method='tools/call', params={'name': 'test_connection', 'arguments': {}, '_meta': {'claudecode/toolUseId': 'toolu_01SqXkUVGvojypc6RaA4UriG', 'progressToken': 14}}, jsonrpc='2.0', id=14)
[DEBUG] mcp.server.sse - Sending session message to writer: SessionMessage(...)
INFO:     POST /messages/?session_id=f3ee2d980809439b97eb35556d220eb1 HTTP/1.1" 202 Accepted
[WARNING] root - Failed to validate request: Received request before initialization was complete
[DEBUG] root - Message that failed validation: method='tools/call' params={'name': 'test_connection', 'arguments': {}, '_meta': {'claudecode/toolUseId': 'toolu_01SqXkUVGvojypc6RaA4UriG', 'progressToken': 14}} jsonrpc='2.0' id=14

Note: The session was just newly created by the same SSE handshake (session_id=f3ee2d98... was generated 11 seconds before). The client did not send initialize between SSE-connect and tools/call — this is observable in the raw POST body capture above.

Reproduction

  1. Have a Python SSE MCP server using the standard mcp.server.sse.SseServerTransport + Server(...).run() pattern (see example in the repo).
  2. Connect with the Claude Code CLI as an MCP client (configure as a SSE-type MCP server in the user's ~/.claude/config).
  3. The server has been running and idle for >1 hour.
  4. From within a Claude Code session, invoke a tool. About half the time, the request comes in without prior initialize.

The first session right after a service restart always works correctly. The problem manifests after the client has gone idle for some time and then resumes — strongly suggests the client is caching session state across what the server considers a session re-establishment.

What I'd like to know

  1. Is this expected client behavior — should the server tolerate tools/call without preceding initialize?
  2. Or should the SDK make the server idempotently re-initialize on first non-init request (a kind of stateless mode for SSE, like StreamableHTTPSessionManager(stateless=True) does for streamable_http)?
  3. Is there a workaround other than monkey-patching _received_request?

Workaround we are using (not great, but it works)

We replaced the RuntimeError with an implicit state transition + warning, as a runtime monkey-patch:

async def _patched_received_request(self, responder):
    match responder.request.root:
        case types.InitializeRequest() | types.PingRequest():
            return await _original(self, responder)
        case _:
            if self._initialization_state != InitializationState.Initialized:
                logger.warning("Implicit initialization (client skipped initialize handshake)")
                self._initialization_state = InitializationState.Initialized
            return await _original(self, responder)

ServerSession._received_request = _patched_received_request

Full module: [linked in our deployment if helpful].

Possibly related

Thanks for the SDK!

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