MCP HTTP-Streamable Server: Init-Handshake fails on reconnect

Resolved 💬 8 comments Opened May 4, 2026 by livsol123 Closed May 23, 2026

MCP HTTP-Streamable Server: Init-Handshake fails on reconnect

Description

When a local MCP server (Streamable HTTP transport, @modelcontextprotocol/sdk) is restarted while a Code-CLI session is active, the Code-CLI client keeps the old mcp-session-id in internal state and continues sending tool calls with this ID. After restart, the server no longer recognizes the ID and returns:

Streamable HTTP error: Error POSTing to endpoint: event: message
data: {"jsonrpc":"2.0","error":{"code":-32000,"message":"Bad Request: Server not initialized"},"id":null}

Expected: On "Server not initialized" response, the client should automatically send a fresh initialize request, rebuild the session, and retry the original tool call.

Actual: Client returns the error to the caller (LLM tool-use). Only a full Code-CLI restart creates a new session — the /mcp slash-command reload does not solve it.

Reproduction

  1. Start Code-CLI session with a configured Streamable-HTTP MCP server
  2. Call any MCP tool (server registers session)
  3. Restart the server process (e.g. systemctl restart wiki-mcp.service)
  4. Call MCP tool again
  5. Expected: success after automatic re-initialize
  6. Actual: "Bad Request: Server not initialized"

Setup

  • Code-CLI version: latest (Opus 4.7 1M-context build, 2026-05)
  • MCP-Server: @modelcontextprotocol/sdk StreamableHTTPServerTransport, deployed as own systemd daemon behind nginx
  • Configured via Anthropic Custom-Connector (claude.ai web UI) — verified claude mcp list lists it as claude.ai <name>: ✓ Connected

Relation to existing issues

  • #43177 "MCP stdio servers never auto-reconnect after disconnect" — same family but stdio transport. This report is HTTP-Streamable (StreamableHTTPServerTransport).
  • #42509 "mcp-proxy.anthropic.com returns 502 after initial handshake" — overlaps Class A symptom but with proxy-side 502. This report's Class A is the JSON-RPC "Server not initialized" from the SDK, plus Class B "Server already initialized" triggered by Anthropic Custom-Connector backend, neither of which #42509 covers.

Why this matters

Server restarts happen during code updates and config patches. Today every restart forces a full Code-CLI restart (losing prompt cache, requiring re-orientation). The MCP standard (JSON-RPC 2.0 + initialize handshake) allows clean re-initialization in principle.

Workaround

Full Code-CLI restart (/exit + new launch). Chat context survives in transcript JSONL. Practical sequence in shared multi-client setup: systemctl restart <mcp-server> → immediately claude mcp list (race) before other clients reconnect.

Possible fixes (for discussion)

  1. Client-Side: On "Server not initialized" response, discard the session-id, send fresh initialize, retry the tool call
  2. SDK Server-Side relaxation: StreamableHTTPServerTransport accepts arbitrary client session-id and initializes lazily on first request (own server-side mitigation tried — non-trivial because of internal state machine)
  3. Protocol clarification: HTTP 410 Gone on unknown session-id instead of JSON-RPC error — gives client a clear signal to re-initialize

Live evidence — two symptom classes on the same setup

Setup with two MCP servers (wiki-mcp and knowledge-mcp) on the same Streamable-HTTP-SDK base, configured via Anthropic Custom-Connector. Two related symptom classes observed repeatedly:

Class A — "Server not initialized" after server restart (Code-CLI client)

Code-CLI with HTTP-MCP server connection. Server restart → tool calls fail with "Server not initialized". Reproducible (2026-05-03, 0830–0852 CEST). Code-CLI restart workaround confirmed.

Class B — "Server already initialized" on connector add / reconnect (Anthropic Custom-Connector backend)

Other path: claude.ai Custom-Connector is added, OAuth flow succeeds, but the backend's first MCP initialize request returns:

HTTP/2 400
content-type: application/json
{"jsonrpc":"2.0","error":{"code":-32600,"message":"Invalid Request: Server already initialized"},"id":null}

Reproducibly triggered when the MCP server (StreamableHTTPServerTransport) still holds a session from the previous client. The Anthropic Custom-Connector backend retries multiple times — all attempts fail with the same error. Connector is never registered.

Live evidence (timestamps CEST):

  • 2026-05-03 14:50 — first knowledge-mcp connector-add attempts fail with verify(knowledge) failed: aud (nginx njs layer). After custom-audience workaround the auth passes, immediately followed by "Server already initialized". Workaround: systemctl restart knowledge-mcp → successful connect.
  • 2026-05-03 20:25 — connector ran stably since 14:53. After ~5h35min server idle (no MCP call), reuse attempt fails with auth error ofid_f46ca96c23d9fd4d. Workaround systemctl restart knowledge-mcp → green.
  • 2026-05-04 04:00–09:11 — secondary client (local OpenClaw gateway with @modelcontextprotocol/sdk@1.29.0) tries auto-reconnect every 10–15 min via health-monitor. 56 consecutive "Server already initialized" errors in /tmp/openclaw/openclaw-2026-05-04.log, all on knowledge-mcp. Server-side: knowledge-mcp.service was up the whole time (Active: active (running) since 2026-05-03 20:28:31 CEST; 14h ago).
  • 2026-05-04 11:22 — direct race-test: sudo systemctl restart knowledge-mcp on the server, then within 1s claude mcp list from Code-CLI → connector flips to ✓ Connected. Direct curl probe a second later with the same Bearer token returns "Server already initialized" — proving that the server post-restart only allows one client to complete initialize, all subsequent clients are locked out until next restart.

Hypothesis

Server-state (StreamableHTTPServerTransport's internal session-map) accumulates or invalidates without client-side resync:

  • Class A: Server forgot the session, client still holds it → "not initialized"
  • Class B: Server holds a session, client (or Anthropic Custom-Connector backend) has none → "already initialized"

Both classes are resolved by server restart → server restart cleans the session-map and allows a fresh init. But the restart only "buys" one fresh client connection — the next client to attempt initialize immediately gets locked out again.

Appendix — Server-side mitigation (limited effect)

Tried in our own server, in StreamableHTTPServerTransport, to reuse the client session-id instead of generating a new one:

sessionIdGenerator: () => sessionId || randomUUID()

Helps with fresh connections after restart, but does not solve the reconnect problem — the transport state machine expects an initialize handshake, and a tool call with an unknown session-id arrives before it.

View original on GitHub ↗

This issue has 8 comments on GitHub. Read the full discussion on GitHub ↗