MCP HTTP client sends reconnect GET missing Accept: text/event-stream, breaking tools/call after successful tools/list (406)
Summary
For a Streamable HTTP MCP server that requires a valid Authorization header on every request (not just initialize), Claude Code's MCP HTTP client successfully completes the full handshake (initialize → notifications/initialized → open SSE stream → tools/list) — but on a later connection/reconnect attempt within the same conversation, it sends a bare GET /mcp as the first request on that new connection (no preceding initialize), and that GET is missing the Accept: text/event-stream header the MCP spec/SDK requires for it. The server correctly rejects it with 406 Not Acceptable, breaking the flow before the client ever issues the intended tools/call.
This reproduced 100% of the time in my testing once the conversation had already done one or two successful rounds of tools/list against the server — the very first exchange in a fresh session always worked; subsequent tool-call attempts against the same registered server consistently hit the 406 and the tool call never completes (no CallToolRequest is ever logged server-side for the failing attempts).
Environment
- Claude Code CLI (current at time of report), Linux
- MCP server: custom Python server built on the official
mcpPython SDK (mcp.server.fastmcp.FastMCP+streamable_http_app()), self-hosted,http://localhost:<port>/mcp - Registered via:
claude mcp add --transport http <name> http://localhost:<port>/mcp --header "Authorization: Bearer <static JWT>" - Auth model: server validates the
Authorizationheader on every request (stateless per-request auth), not just at session establishment — this is a deliberate, spec-legal design (nothing in the MCP spec ties auth validity to session lifetime)
Server-side evidence (from the MCP server's own request log)
Two full, successful cycles, each on its own TCP connection (same source port across all 4 requests in a cycle):
auth_success (org_id, role correctly extracted from Authorization header)
Created new transport with session ID: <session-a>
POST /mcp -> 200 OK (initialize)
auth_success
POST /mcp -> 202 Accepted (notifications/initialized)
auth_success
GET /mcp -> 200 OK (opens SSE notification stream — correct Accept header present)
auth_success
POST /mcp -> 200 OK (tools/list, "Processing request of type ListToolsRequest")
This full cycle succeeded twice, ~20 seconds apart. Then, on the next attempt (right where the client should proceed to actually call a tool):
auth_success
Created new transport with session ID: <session-c>
GET /mcp -> 406 Not Acceptable
No initialize preceded this GET, and it's missing text/event-stream in Accept (confirmed directly against the MCP SDK's own validation logic in mcp/server/streamable_http.py::_handle_get_request, which checks Accept before anything else and returns exactly "Not Acceptable: Client must accept text/event-stream" → 406 when it's absent). No CallToolRequest is ever logged for this or later attempts — the tool invocation never happens.
Why this isn't a server bug
I independently reproduced both the success and failure paths with raw curl, matching the client's own requests header-for-header:
curlwith the correctAccept: application/json, text/event-streamonPOSTandAccept: text/event-stream(or the combined value) onGET→ always200, full tool calls succeed, real data returned.curlreproducing the client's failing request (bareGET /mcp, no priorinitialize,Acceptheader omitted or set to onlyapplication/json) → reliably406, matching the server logs exactly.
The MCP Python SDK's Accept-header validation here is the spec's own strictness, not custom server code — any compliant streamable-HTTP server built on this SDK would reject the same malformed request identically.
Impact
claude mcp list/claude mcp get <name>//mcpall correctly report✔ Connected(the initial handshake genuinely succeeds), which is misleading — the connection is not actually usable for subsequent tool calls.- Tool calls silently fail to ever start (no error surfaced to the model beyond the generic transport 406 in the hook/tool-call layer) rather than a clear "reconnect failed" message.
- Affects any Streamable HTTP MCP server that validates auth per-request (a legitimate, spec-compliant pattern) combined with a static
--header-configured token — the client's reconnect path doesn't appear to consistently reuse the same request-construction logic as its initial-handshake path.
Possibly related (different specific symptom, same general subsystem — MCP HTTP client's handling of the optional server-initiated GET/SSE stream and reconnection)
- #70386 — HTTP MCP client drops
Mcp-Session-Idheader ontools/list - #67194 — Streamable HTTP client mis-correlates a
405GET response body with a pendingtools/list - #60061 — Claude Code hangs MCP tool calls indefinitely after an SSE drop, no reconnect attempted
All four reports point at the same area of the client: handling of the optional GET/SSE channel and session continuity across multiple requests to a Streamable HTTP MCP server. This one is narrower — a missing Accept header specifically on a reconnect GET — and comes with a clean, deterministic, 100%-reproducible server-side log trail.
Suggested fix
The reconnect/resume code path for an existing MCP HTTP session should construct its GET /mcp request the same way the initial-handshake code path does — including Accept: text/event-stream (or the combined application/json, text/event-stream value) every time, not just on the first connection of a session.