What happens to an MCP tool call when the connection drops?
AI Architect
Key takeaways
- Resumable SSE streams via Last-Event-ID are no longer supported on Streamable HTTP.
- On Streamable HTTP, closing the response stream is itself the cancellation signal for that request.
- A server receiving a cancellation only SHOULD stop processing, and MAY ignore it if the work already completed.
- Clients must re-issue a lost request with a new JSON-RPC id, which removes the last thing a server could deduplicate on.
- The 2026-07-28 tools page defines no idempotency mechanism, so retry safety is entirely the server author's problem.
Revision 2026-07-28 of the Model Context Protocol removed two things that used to cover for a flaky network: protocol-level sessions, and resumable response streams. The combination changes what a dropped connection means, and the specification is direct about the consequence in a way most migration notes are not.
What does MCP do when a tool call’s connection drops?
It treats the drop as cancellation, discards the request, and asks you to start over.
Three rules combine. The
Streamable HTTP page
states that “Resumable SSE streams via Last-Event-ID are not supported”. The
cancellation page
states that “Closing the SSE response stream is the cancellation signal” and that
“The server MUST treat a client disconnect as cancellation of that request”.
And the
changelog
says a broken response stream “loses the in-flight request; clients MUST
re-issue it as a new request with a new request ID.”
A dropped stream, from both sides
Show as text
| # | From | To | Message |
|---|---|---|---|
| 1 | MCP client | MCP server | POST tools/call (id 7). Server opens an SSE stream for this request. |
| 2 | MCP server | MCP server | Work starts, side effect commits. The charge, send or write has now happened. |
| 3 | MCP client | MCP server | Stream closes, by timeout or network. Server MUST read this as cancellation. |
| 4 | MCP client | MCP server | POST tools/call (id 8, same arguments). A fresh request. Nothing links it to id 7. |
From the client’s side the outcome is a request with no response. From the server’s side it is a cancellation that may have arrived after the work was done. Neither side can tell the other which happened.
Why did MCP remove SSE stream resumability?
Because resumability was the last piece of per-connection state, and the whole revision exists to remove that.
SEP-2575 took out the initialize handshake, the Mcp-Session-Id header and the
standalone GET stream in one move, so a server no longer holds anything that
belongs to a particular client connection. Resumability was incompatible with
that: replaying missed events after a reconnect requires the server to remember
what it already sent, keyed to a connection that has gone away. The
stateless design is what makes an MCP
server ordinary horizontally scaled HTTP infrastructure, and buffered per-stream
event history is exactly the thing that stops it being that.
The trade is real and the spec made it deliberately. Servers get to be stateless and load-balanced. Clients get a transport where an interrupted request is simply gone. That is a defensible engineering choice, and it moves a cost rather than removing one.
Does a client timeout cancel an MCP tool call?
On Streamable HTTP, yes, and this is the part that surprises people.
The cancellation page tells implementations they SHOULD establish timeouts for all sent requests, and spells out what a timeout does on each transport. On Streamable HTTP it means “closing the response stream for the request, which constitutes cancellation”. So an ordinary client-side timeout is not a local decision to stop waiting. It reaches the server and asks it to abandon work.
What the server does next is weaker than it looks. Servers receiving cancellation SHOULD stop processing, free resources and send no response. They MAY ignore the cancellation entirely when the referenced request is unknown, when processing has already completed, or when the request cannot be cancelled. The page adds that cancellations “may arrive after request processing has completed, and potentially after a response has already been sent”.
A timeout tuned for a fast tool and applied to a slow one therefore cancels work that was going to succeed, without telling you whether it did.
Does MCP have an idempotency key for tool calls?
No. The 2026-07-28 tools page defines none, and the mechanism people reach for instead does not do the job.
tools/call carries name, arguments, and under the multi round-trip pattern
inputResponses and requestState. There is no field for a caller-supplied
key, no guidance on repeating a call, and no requirement that a server
deduplicate anything. The annotations object is described only as “Optional
properties describing tool behavior”, and clients are told they “MUST
consider tool annotations to be untrusted unless they come from trusted
servers”. A hint you are instructed to distrust is not a safety mechanism.
The instruction to re-issue with a new request ID closes the last door. The
JSON-RPC id was the one value a server could have used to spot a repeat, and
the spec requires it to change. The retry arrives looking exactly like a second
independent call, because on the wire that is what it is.
How do you make an MCP tool call safe to retry?
By moving the key into the place the protocol does leave open: your own arguments object.
What has to exist for a retry to be safe
Show as text
| # | Layer | Note |
|---|---|---|
| 1 | Mint the effect ID on the client, before the call | The only ID that survives a re-issue. |
| 2 | Pass it as an ordinary tool argument | MCP has no field for it. Use your schema. |
| 3 | Record the ID in the same commit as the work | Written after, and a crash breaks it. |
| · | everything above is your job (breakpoint) | The protocol supplies none of it. |
| 4 | Re-issue after a drop with the same effect ID | New JSON-RPC id, same business key. |
| 5 | Server replays the stored result | A replay, rather than a second charge. |
The specification points at this pattern for a neighbouring problem. Its
“Stateful Tools” section is explicit that “The protocol has no concept of a state
handle; from the wire’s perspective a handle is an ordinary string in a tool
result and an ordinary argument to subsequent tool calls.” The same escape hatch
carries an idempotency key. Put an effect_id in the tool’s inputSchema, mark
it required, and have the caller generate it once and reuse it on every attempt.
Then make the server honour it: record the key inside the same transaction as the effect, and on a repeat return the stored result rather than an error. That last detail matters more than it sounds, because the caller here is a model. A conflict error invites it to reason about a failure that never happened, and a replayed result lets it continue.
Should MCP define this, or should your server?
MCP should define it, and the argument for leaving it out is weaker after this revision than it was before.
The usual case for keeping a protocol thin is that the application layer knows more. That holds for what a tool does. It does not hold for whether a request was delivered, which is a transport question, and the transport just changed in a way that makes the answer unknowable. Removing resumability was the right call for scaling. Removing it without adding any way to identify a repeated call pushed a correctness problem onto every server author individually, and most of them will not notice until a customer is billed twice.
An optional idempotencyKey on CallToolRequest params, with a rule that
servers supporting it declare so in capabilities, would cost the spec very little
and would let clients retry safely by default. Until something like that exists,
treat every write-shaped tool in your server as unsafe to repeat unless you built
the key yourself.
What is still open here?
Whether the SDKs will paper over this, and whether that would help or hurt.
Nothing published says how the official client libraries intend to handle a dropped stream. An SDK that silently re-issues the request would give every existing server the double-execution problem without anybody choosing it. An SDK that surfaces the drop as an error leaves a rough edge that agent frameworks will each solve differently, which is how a de facto standard gets set badly.
Worth watching, and worth testing rather than assuming. Point a client at a
server that logs every tools/call, kill the connection mid-call, and count the
log lines. That single test tells you what your stack actually does, and it is
the kind of thing worth knowing before a
connected server is doing anything that costs
money.
Do this
Make an MCP tool call safe to retry
The protocol gives you nothing here, so every step below is something your server and your tool schema have to carry themselves.
Sort your tools by whether repeating one costs anything
A read is free to retry. A write that charges, sends or provisions is the set that needs the rest of this. Most servers have fewer of the second kind than they expect.
Add an effect ID to the input schema of every such tool
MCP has no field for this, so it goes in your own arguments object. Require it, document it in the tool description, and reject calls that omit it.
Have the client mint that ID before the first attempt
An ID the server generates, or one the model invents fresh each time, changes on every retry and buys nothing. The caller creates it once and reuses it across attempts.
Record the ID in the same transaction as the effect
Writing the key after the work leaves a window where a crash loses the record and keeps the side effect. Same commit, or the guarantee is decorative.
Return the stored result on a repeat, instead of an error
A retry that gets a conflict error forces the model to reason about a failure that did not happen. Replay the original result and let the agent carry on.
Set client timeouts per tool rather than per server
A timeout closes the stream and cancels the request, so one global value applied to a slow tool manufactures the exact race this article is about.
Say in the tool description what a repeat does
The model decides whether to retry. Description text is what it reads, and a sentence there changes behaviour more reliably than an annotation the client is told to distrust.
Frequently asked questions
- Can I resume a broken MCP stream where it left off?
- No. The Streamable HTTP page states plainly that resumable SSE streams via Last-Event-ID are not supported. The Last-Event-ID header and SSE event IDs were removed in revision 2026-07-28, and a server that receives the header should ignore it.
- Does my client timeout cancel work on the server?
- On Streamable HTTP, yes. The cancellation page says a timeout means closing the response stream for that request, and that closing the stream constitutes cancellation. On stdio the client sends notifications/cancelled instead.
- Is cancellation guaranteed to stop the work?
- No. Servers SHOULD stop processing and free resources, and MAY ignore a cancellation when processing has already completed or the request cannot be cancelled. The spec also notes cancellations can arrive after a response has been sent.
- Why must the retry use a new request ID?
- The changelog for revision 2026-07-28 states that a broken response stream loses the in-flight request and clients MUST re-issue it as a new request with a new request ID. JSON-RPC ids are per-request, and reusing one after a drop would be ambiguous on the wire.
- Do tool annotations solve this?
- They do not. Annotations are optional properties describing tool behaviour, and the spec tells clients to treat them as untrusted unless they come from a trusted server. Nothing in them makes a call safe to repeat.