MCP HTTP-Streamable Server: Init-Handshake fails on reconnect
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
- Start Code-CLI session with a configured Streamable-HTTP MCP server
- Call any MCP tool (server registers session)
- Restart the server process (e.g.
systemctl restart wiki-mcp.service) - Call MCP tool again
- Expected: success after automatic re-initialize
- Actual:
"Bad Request: Server not initialized"
Setup
- Code-CLI version: latest (Opus 4.7 1M-context build, 2026-05)
- MCP-Server:
@modelcontextprotocol/sdkStreamableHTTPServerTransport, deployed as own systemd daemon behind nginx - Configured via Anthropic Custom-Connector (claude.ai web UI) — verified
claude mcp listlists it asclaude.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)
- Client-Side: On
"Server not initialized"response, discard the session-id, send freshinitialize, retry the tool call - SDK Server-Side relaxation:
StreamableHTTPServerTransportaccepts arbitrary client session-id and initializes lazily on first request (own server-side mitigation tried — non-trivial because of internal state machine) - 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-mcpconnector-add attempts fail withverify(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. Workaroundsystemctl 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 onknowledge-mcp. Server-side:knowledge-mcp.servicewas 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-mcpon the server, then within 1sclaude mcp listfrom 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 completeinitialize, 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.
This issue has 8 comments on GitHub. Read the full discussion on GitHub ↗