Do not retry a side-effecting tool when a transport failure leaves it ambiguous whether the original call already ran; a blind retry can double-charge or double-write.
Think about hitting Submit on a checkout page and the screen freezing. Did the payment go through, or not? If you click Submit again to be safe, you might get charged twice. The safe move is to check the order history first, not blindly resubmit. A production MCP client faces the same problem. When a tool call times out, the client cannot tell whether the server already ran the tool before the connection dropped. For a read like list files, retrying is harmless. For a write like send payment, retrying could fire it twice. So the rule is: retry freely when the action is safe to repeat, but never blindly retry an action that changes the world when you are unsure it already happened.
Concept explanation~2 min read
Everything you need to truly understand this topic: intuition, mechanics, step by step explanation, code, formulas, and worked example. Click to expand.
Concept explanation~2 min read
Everything you need to truly understand this topic: intuition, mechanics, step by step explanation, code, formulas, and worked example. Click to expand.
Retry handling looks trivial until a tool call moves money. The naive design wraps every MCP tools/call in a retry loop with exponential backoff and ships it. That works for reads and quietly corrupts state for writes. This question probes whether a candidate understands that retry safety is a property of the operation, not a property of the error code alone.
The core tension is delivery semantics. Network transports give you at-most-once or at-least-once delivery, never exactly-once for free. A retry policy is how you choose between them per tool. At-most-once means you never duplicate but you may lose work. At-least-once means you never lose but you may duplicate. Exactly-once is what you actually want, and you only get it by adding deduplication on top, not from the transport itself. Get it wrong on a payment tool and a single dropped connection becomes a duplicate charge.
MCP makes this concrete because a single host session can hold both read-only and side-effecting tools, fanned out across several connected servers. A uniform retry policy that treats them all like an HTTP GET is the trap. This deep dive separates the two kinds of failures an MCP client sees, explains why an ambiguous timeout is the genuinely dangerous case, shows how idempotency keys let you retry writes safely, and ends with a layered policy you can defend in an interview.
Two failure classes the client must distinguish
An MCP client receives failures in two structurally different shapes, and conflating them is the most common production bug.
A protocol error is a JSON-RPC error object. The response carries an error field with a code and message. Codes like -32700 (parse error), -32600 (invalid request), -32601 (method not found), and -32602 (invalid params) are defined by JSON-RPC. They mean the request itself was malformed or the method does not exist. These are permanent, design-time bugs, not runtime blips. Retrying a -32601 a hundred times still finds no method, and each retry just wastes a backoff window before you surface the real problem.
A tool error is completely different. The call succeeded at the protocol level, the server ran the tool, and the tool decided it failed. MCP surfaces this as a normal tools/call result whose payload sets isError: true with content describing what went wrong. This is a logical failure, like an API returning a validation message: bad arguments, a missing record, a downstream refusal. You surface it to the model or the user so the next turn can correct course; you do not treat it as a transport hiccup and retry it. A model that sees the isError content can often fix its own arguments and call again, which is a deliberate retry, not a blind one.
The rule of thumb: an error object means the message never reached the tool, an isError result means it did. Different cause, different response. Getting this split wrong is how teams end up hammering a server with retries for a request that was never going to succeed.
Situations where this technique stops working.
2–4 min · Everything important, quickly.
| Failure | Shape | Retry? |
|---|---|---|
| Transport timeout, read-only tool | No response / socket drop | Yes, with backoff |
| Transport timeout, side-effecting tool | No response / socket drop | Only with an idempotency key |
| HTTP 503 / 5xx | Server overloaded | Yes if safe to repeat |
| JSON-RPC -32601 / -32602 | Protocol error object | No, permanent |
| Tool result with isError true | Valid result, logical failure | No, surface it |
Real products, models, and research that use this idea.
- Stripe's API uses Idempotency-Key headers so a retried charge after a timeout settles exactly once; an MCP payment tool should mirror this.
- Claude Code and Cursor MCP clients retry transient stdio and HTTP transport errors but surface isError tool results straight to the model.
What an interviewer would ask next. Try answering before peeking at the approach.
QHow would you design an idempotency key scheme so a retried side-effecting MCP tool call settles exactly once?
Mint a UUID per logical operation on the client, pass it on every retry, and have the server store keys with results so a duplicate key returns the cached outcome instead of re-executing.
Red flags & common mistakes
The phrases that signal junior thinking. Click to expand.
Red flags & common mistakes
The phrases that signal junior thinking. Click to expand.
Retrying every failure with blanket backoff. That double-fires non-idempotent tools whenever a timeout hides a call that actually succeeded.
60 second bullets to scan on the way to the call.
The difference between a protocol error and a tool-level isError result
Why an ambiguous timeout makes a side-effecting retry unsafe
Primary sources. Browse if you want the original framing.
Same topic, related formats. Practice these next.