DeepThinking AI

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

A dropped stream, from both sidesSequence diagram between MCP client and MCP server. 1. MCP client to MCP server: POST tools/call (id 7). 2. MCP server to MCP server: Work starts, side effect commits. 3. MCP client to MCP server: Stream closes, by timeout or network. 4. MCP client to MCP server: POST tools/call (id 8, same arguments).MCP clientMCP serverPOST tools/call (id 7)Server opens an SSE stream for this request.Work starts, side effect commitsThe charge, send or write has now happened.Stream closes, by timeout or networkServer MUST read this as cancellation.POST tools/call (id 8, same arguments)A fresh request. Nothing links it to id 7.
Show as text
A dropped stream, from both sides. Sequence diagram between MCP client and MCP server. 1. MCP client to MCP server: POST tools/call (id 7). 2. MCP server to MCP server: Work starts, side effect commits. 3. MCP client to MCP server: Stream closes, by timeout or network. 4. MCP client to MCP server: POST tools/call (id 8, same arguments).
#FromToMessage
1MCP clientMCP serverPOST tools/call (id 7). Server opens an SSE stream for this request.
2MCP serverMCP serverWork starts, side effect commits. The charge, send or write has now happened.
3MCP clientMCP serverStream closes, by timeout or network. Server MUST read this as cancellation.
4MCP clientMCP serverPOST tools/call (id 8, same arguments). A fresh request. Nothing links it to id 7.
The client never learns whether the work committed before the stream closed. The re-issued call carries a new JSON-RPC id, so the server sees two unrelated requests rather than one request and its retry.

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

What has to exist for a retry to be safeDiagram: 6 ordered layers. Mint the effect ID on the client, before the call, then Pass it as an ordinary tool argument, then Record the ID in the same commit as the work, then everything above is your job (breakpoint), then Re-issue after a drop with the same effect ID, then Server replays the stored result.1Mint the effect ID on the client, before the callThe only ID that survives a re-issue.2Pass it as an ordinary tool argumentMCP has no field for it. Use your schema.3Record the ID in the same commit as the workWritten after, and a crash breaks it.everything above is your job4Re-issue after a drop with the same effect IDNew JSON-RPC id, same business key.5Server replays the stored resultA replay, rather than a second charge.
Show as text
What has to exist for a retry to be safe. Diagram: 6 ordered layers. Mint the effect ID on the client, before the call, then Pass it as an ordinary tool argument, then Record the ID in the same commit as the work, then everything above is your job (breakpoint), then Re-issue after a drop with the same effect ID, then Server replays the stored result.
#LayerNote
1Mint the effect ID on the client, before the callThe only ID that survives a re-issue.
2Pass it as an ordinary tool argumentMCP has no field for it. Use your schema.
3Record the ID in the same commit as the workWritten after, and a crash breaks it.
·everything above is your job (breakpoint)The protocol supplies none of it.
4Re-issue after a drop with the same effect IDNew JSON-RPC id, same business key.
5Server replays the stored resultA replay, rather than a second charge.
Every layer above the divider is server and schema work that the specification neither defines nor requires. The two below it are the only parts that look like protocol behaviour, and they only work because of the layers above.

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.

  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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.

  6. 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.

  7. 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.

Sources

  1. Streamable HTTP transport, revision 2026-07-28Model Context Protocol
  2. Cancellation, revision 2026-07-28Model Context Protocol
  3. Tools, revision 2026-07-28Model Context Protocol
  4. Key changes, revision 2026-07-28Model Context Protocol

mcpreliabilityidempotencyretriesstreamable-http