Streamable HTTP MCP: after the standalone GET SSE stream drops and two retries fail, Claude Code never reopens it, so server notifications are lost for the rest of the session
Environment
- Claude Code 2.1.296 on Linux
- MCP server on @modelcontextprotocol/sdk 1.32.1, Streamable HTTP transport, no event store
What happens
After initialize, Claude Code opens the standalone GET SSE stream that the spec allows for server-initiated messages. If that stream closes, Claude Code retries about 1s later and again about 1.5s later. If both retries fail, it never opens a GET stream again for the life of the session.
POST requests keep working, so the session looks healthy. But from then on, every server notification that is not tied to a request is lost. That includes notifications/tools/list_changed, so a server that changes its tool list mid-session is never heard. A brief network blip or a proxy restart is enough to trigger this. Only /mcp reconnect recovers.
Steps to reproduce
- Run a Streamable HTTP MCP server that can change its tool list at runtime and send
notifications/tools/list_changedwith no related request id. - Connect Claude Code and change the tool list once. It works: the notification arrives on the GET stream and the new tools are callable.
- Make the server close the GET stream, then answer the next GET requests with 503.
- Watch Claude Code make two GET retries and then stop.
- Change the tool list again from a tool call.
Expected
Claude Code reopens the GET stream once the server is reachable again. For example, it keeps retrying with capped backoff while the session is alive, or reopens the stream after the next successful POST. It then re-fetches tools/list, and the new tools are callable.
Actual
The notification goes out with no GET stream open, so the server's transport drops it. Claude Code never re-fetches tools/list. ToolSearch reports "No matching deferred tools found", and direct calls fail with "No such tool available". In the same harness with the GET stream left open, three tool-list switches in a row all worked.
Likely cause
The TypeScript SDK client's StreamableHTTPClientTransport defaults reconnectionOptions.maxRetries to 2 (dist/esm/client/streamableHttp.js in 1.32.1), and after that it gives up with "Maximum reconnection attempts (2) exceeded." Nothing in Claude Code appears to reopen the stream afterwards.
Server-side workaround
Send list_changed as a request-related notification, using extra.sendNotification inside the tool handler that changed the tool list. It then travels on that call's own POST response stream, which is open by definition.