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.
8 Comments
Found 3 possible duplicate issues:
This issue will be automatically closed as a duplicate in 3 days.
🤖 Generated with Claude Code
Update — 2026-05-05: Confirmed this bug is not server-specific.
Reproduced today on a second independent local MCP server (
wiki-mcp, separate from the originalknowledge-mcpreport) using@modelcontextprotocol/sdk'sStreamableHTTPServerTransport. Aftersystemctl restart wiki-mcp, the Claude Code client returned the same"Bad Request: Server not initialized"error on the next tool call — identical failure mode. Two independent servers, same client, same symptoms. The bug is in the Claude Code MCP client (likely a stale session-id cache that survives the upstream server's restart), not in any individual server.Differential observation: a parallel Node test client using
@modelcontextprotocol/sdk/clientdirectly (no stale state) connected to the same restartedwiki-mcpserver and initialized cleanly on the first request. This supports the hypothesis that the client-sideStreamableHTTPClientTransportdoesn't invalidate its session-id when receiving a "Server not initialized" response.---
Re: the auto-closure duplicate suggestion — not a duplicate of #55228 (which describes a 5-attempt reconnect cap as the visible failure mode), but related. Both may be aspects of the same HTTP Streamable transport fragility on the client side, with this issue (#55970) capturing the init-handshake state layer and #55228 capturing the retry-cap behavior. #55943 is OAuth-specific (different surface); #42509 is mcp-proxy 502 (different transport path). Worth triaging separately.
Update 2026-05-07 — additional finding from continued reproduction
While debugging a recurring
✗ Failed to connectagainst one of our local MCP daemons (knowledge-mcp), we identified a second, orthogonal root cause that produces the same surface symptom as this issue but is in fact a server-side bug — not the client-side reconnect issue described in the original report.Two orthogonal classes
| Class | Root cause | Fix scope |
|---|---|---|
| (A) Daemon single-instance bug | MCP server built with one global
StreamableHTTPServerTransportinstance instead of a per-mcp-session-idmap. First clientinitializemarks the transport as initialized; every subsequentinitialize(e.g. after Code-CLI restart) is rejected with{"error":{"code":-32600,"message":"Invalid Request: Server already initialized"}}. Daemon also returns plain JSON instead of SSE. | Server-side. Fixed today by switching to per-session pattern (see below). || (B) Code-CLI stale session-id cache after server restart | Code-CLI client retains the old
mcp-session-idinternally; after server restart the server no longer knows the id; client emits tool calls with the stale id and getsServer not initialized; client does not auto-emit a freshinitialize. | This issue (#55970). Still open. |Why I'm reporting class (A) here
We initially mis-attributed all
✗ Failed to connectcases onknowledge-mcpto class (B). It turned out onlywiki-mcphad the (B) symptom;knowledge-mcphad (A) on top of (potentially) (B). After fixing (A),knowledge-mcp: ✓ Connectedon first attempt — so (A) was a real, distinct bug masking (B)-style observations on that daemon.That said, (B) is unchanged on
wiki-mcp, which already uses the per-session pattern. Aftersystemctl restart wiki-mcpmid-session, Code-CLI still returnsServer not initializedon the next tool call until a full/exit+clauderestart. So the original report stands — Code-CLI does not auto-re-initializeon stale-session-id rejection.Server-side fix (class A — for reference, not part of this issue)
Anti-pattern in our
knowledge-mcpdaemon (server.js):Fixed by mirroring our
wiki-mcpdaemon's per-session pattern:After this patch, two consecutive
initializerequests with differentMcp-Session-Idheaders both succeed with correct SSE responses. No"already initialized"rejection.Suggestion for the SDK / docs
If the official position is that
StreamableHTTPServerTransportis a single-session abstraction, this is fine — but the SDK or docs could be more explicit: a Streamable-HTTP MCP server must maintain a per-mcp-session-idtransport map; sharing one transport across requests is not just a performance choice, it deadlocks reconnect entirely. A canonical multi-session example in the SDK README would have saved us the diff.For class (B) specifically (Code-CLI side), the proposed client-side fix in the original report — on
Server not initializeddiscard the cached session-id and emit a freshinitialize— would resolve our remainingwiki-mcpreconnect symptom.Happy to provide repro snippets for either class if helpful.
I believe that I'm hitting this too.
Confirming the same root cause on a Rust remote MCP server (
rmcp 1.6). Our setup produces a slightly different on-the-wire signature -- worth adding to the report so the proxy-side fix covers both variants:Different status, same bug. Our server returns plain HTTP 404 (no JSON-RPC body) when a request arrives with an
Mcp-Session-Idit doesn't recognize -- per the MCP2025-03-26Streamable-HTTP transport spec, that 404 is the explicit signal for "session gone, please reinitialize." The Claude-Code proxy doesn't follow that signal; instead it surfaces:to the client and gets stuck. So the same client-side defect manifests as
-32000 "Server not initialized"against@modelcontextprotocol/sdkJS servers (your case) and as-32600 "Invalid content from server"againstrmcpRust servers (ours). Both are the proxy failing to discard the stale session id and reinit.Concrete evidence -- from our server's per-request logs after a v0.8.1 -> v0.8.2 package upgrade restarted the process. Four consecutive
tools/callPOSTs from the same client, all carrying a session id minted by the pre-upgrade process:All four return
status=404 elapsed_ms=0-- deterministic, sub-millisecond, server-side. Body size and parallelism are not relevant (348 bytes fail identically to 5497 bytes). The proxy surfaces all four as the-32600error to the user.One difference in workaround: in our setup
/mcp reconnect(the slash-command) does recover the connection -- it forces the proxy to drop its cachedMcp-Session-Idand re-issueinitialize. You report/mcpreload doesn't help for the JS SDK case, only a fullclauderestart does. Worth a separate look: either/mcp reconnectsemantics differ between custom-connector and CLI-configured remote servers, or the JS SDK fails the subsequentinitializefor an unrelated reason once it's already issued one.Suggested fix (same as yours): in the proxy's
POST /mcpretry path, treat the following two responses identically -- both mean "session gone":HTTP 404on a request carrying anMcp-Session-Id(MCP2025-03-26spec).-32000and"Server not initialized"(JS SDK behavior).In either case: discard the cached session id, run
initializeagainst the same endpoint, replay the original tool call. Filed as #59410 with full context before finding this issue; closing it as a duplicate.Confirming on a Python
mcpSDK (FastMCP) server runtime — and adding a new datum: the failure can occur without a server restart.Hitting the same client-side defect against a different server stack:
mcp==1.26.0(mcp.server.fastmcp.FastMCPoverstreamable_http_app()), local HTTP, token-gated.2.1.143, Linux.HTTP 404body{"jsonrpc":"2.0","id":"server-error","error":{"code":-32600,"message":"Session not found"}}— emitted atmcp/server/streamable_http_manager.py:280-297when an incomingMcp-Session-Idis not in_server_instances. This matches @ptomsich'srmcp404 variant and is the exact MCP2025-03-26spec signal for "session gone, reinitialize."New observation: server-restart is not necessary to trigger the client-side stall. Every prior repro on this thread (and on the just-closed-as-dup #60949 / #59442) starts with
systemctl restart/process restart/deploy. In my case the server process had continuous uptime of 5+ days (single PID, no restarts, journal continuous) when the client's session went stale mid-flight. Timeline from the client transcript:13:55:48ZClaude Code session starts, MCPinitializesucceeds,Mcp-Session-Idcached.13:56:33Z–13:59:24ZFour successfultools/callround-trips (list_projects,get_project_status,write_session_note×2).14:00:01ZNextlist_projectsreturnsHTTP 404 / "Session not found". Every subsequent call from this Claude Code instance returns the same. The session is permanently bricked for the remaining 5+ days the user keeps it open.Server-side: no
Cleaning up crashed session ...log entry was emitted in that window (the only path that removes an entry from_server_instances, atstreamable_http_manager.py:266-271, logs unconditionally when it fires). So either (a) the entry was evicted without going through that path, or (b) the client started sending anMcp-Session-Idthe server never issued. Either way, the recovery contract is the same — on404/code -32600 "Session not found", drop the cached id, re-initialize, retry once — and that is the contract Claude Code is still not honoring.Other tracking:
DELETEs its session on shutdown, so_server_instancesaccumulates dead entries indefinitely on the server side. Likely the same code path./mcp reconnectdoes not actually reinitialize the HTTP transport in 2.1.143 — only full process exit recovers (confirming #60949's observation on a separate runtime).Thanks for the report. The client now detects when a Streamable HTTP server no longer recognizes the session (after a server restart) and automatically re-initializes instead of surfacing the error. This covers both the spec-standard 404 response and the 400 "Server not initialized" response that some server implementations return.
For the long-term fix in the TypeScript SDK, see modelcontextprotocol/typescript-sdk#2125.
This issue has been automatically locked since it was closed and has not had any activity for 7 days. If you're experiencing a similar issue, please file a new issue and reference this one if it's relevant.