[BUG] MCP tools/call is re-sent after '-32000 Connection closed' mid-call, so a non-idempotent tool runs twice
Preflight Checklist
- [x] I have searched existing issues and this hasn't been reported yet
- [x] This is a single bug report
- [x] I am using the latest version of Claude Code
What's Wrong?
When a Streamable HTTP MCP connection drops while a tools/call is in flight (MCP error -32000: Connection closed), Claude Code treats the drop as session expiry, re-initializes, and re-sends the same tool call. A dropped connection does not prove the server never received the request. In our case the first POST had already reached the server and the tool had run; the retry ran it a second time.
The model saw exactly one tool_use and one tool_result (the retry's), so it had no way to know the tool executed twice. Our tool runs commands on remote machines: the first run performed a destructive file operation, and the retry ran the same command again against the changed state.
This differs from the recovery added in #55970, which retries after the server answers HTTP 404 for an unknown Mcp-Session-Id. A 404 proves the server rejected the request without running it, so that retry is safe. A connection closed mid-call proves nothing about execution. Related but separate: #95018 (the retry covers only tools/call) and #96733 (two concurrent initializes during recovery).
What Should Happen?
- Retry a
tools/callautomatically only when the failure proves the server did not process it: an HTTP 404 (session not found) answer to the POST, or a transport that was already closed before the request was written. - When the connection drops after the request was sent, return the error to the model instead of re-sending it, worded so the model knows the tool may have run (for example "connection lost after the call was sent; the tool may have executed").
- Optionally, allow the automatic retry for tools whose MCP annotations mark them safe to repeat (
readOnlyHint: trueoridempotentHint: true), and never for tools without them.
Error Messages/Logs
Client MCP log (mcp-logs-<server>/*.jsonl), UTC, server and tool names replaced:
13:54:38.264 Calling MCP tool: <server>__<tool>
13:54:39.056 Tool '<server>__<tool>' failed after 0s: MCP error -32000: Connection closed
13:54:39.056 MCP session expired during tool call (connection closed), clearing connection cache for re-initialization
13:54:39.057 Retrying tool '<server>__<tool>' after session recovery
13:54:39.058 Initializing HTTP transport to https://<gateway>/<server>/mcp
13:54:39.818 Successfully connected (transport: http) in 760ms
13:54:39.819 Calling MCP tool: <server>__<tool>
13:54:43.283 Tool '<server>__<tool>' completed successfully in 3s
Server side, the same minute: the first request arrived about 0.6 s after the call started and ran (queued 13:54:38.834, completed 13:54:40.330). The retried request arrived with byte-identical arguments on a new JSON-RPC id and a new MCP session and ran again (queued 13:54:41.055, completed 13:54:43.112). Two more retries followed the same pattern on another session that morning (13:52:30 and 13:53:29 UTC).
Steps to Reproduce
- Connect Claude Code to a Streamable HTTP MCP server with a tool that has a visible side effect (for example, appends a line to a file and sleeps 2 s).
- Call the tool, and close the HTTP connection from the server or a proxy after the request is received but before the response is written.
- Claude Code logs "MCP session expired during tool call (connection closed)" and "Retrying tool ... after session recovery", then calls the tool again.
- The side effect happened twice; the model sees one successful result.
Claude Model
Not model-specific (client transport behavior).
Is this a regression?
I don't know.
Claude Code Version
2.1.283 (User-Agent claude-code/2.1.283 (sdk-ts, agent-sdk/0.3.260))
Platform
Claude Code through the Agent SDK
Operating System
Linux (Node v26.3.0)
Terminal/Shell
Other
Additional Information
We have worked around it server-side by answering a re-sent call from an intent ledger within a short window. Every server with a non-idempotent tool is exposed until the client stops re-sending after an ambiguous failure.