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
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions