An AI client retries a tool call for all kinds of ordinary reasons: a request timed out before the response came back, a connection dropped mid-call, or the model itself decided the first attempt’s result was ambiguous and tried again to be sure. None of that is unusual, and none of it is a bug in the client. The bug, if there is one, is in the tool: if calling it twice with the same arguments does the thing twice — sends the email twice, creates two tickets, charges the card twice — a routine retry becomes a real mistake with no error message attached. This is the idempotency problem, and it’s one that most MCP tools don’t handle, because nothing about writing a tool forces you to think about it.
What “idempotent” means for a tool
A tool call is idempotent if calling it more than once with the same input produces the same effect as calling it once. Looking up an order, listing recent messages, checking a deploy’s status — these are naturally idempotent, because reads don’t change anything no matter how many times you repeat them. The risk lives entirely in write tools: create, send, charge, delete. create_invoice called twice with identical arguments doesn’t return the same invoice twice — it usually creates two invoices, because from the server’s point of view, it just received two separate requests to create something.
Why the retry happens at all
MCP runs tool calls over JSON-RPC, and JSON-RPC gives every request a unique ID. That ID identifies the request, though, not the action behind it — it lets the client match a response to the call it sent, not tell a server “you already did this one.” If a response never arrives (a timeout, a dropped connection, a proxy that hiccups), the client has no reliable way to know whether the action happened before it failed or after. The safe assumption from the client’s side is often “it might not have gone through, try again” — and an agent making that call on your behalf will make exactly that same assumption, because it’s the reasonable one.
There’s a second, less obvious source of retries that has nothing to do with the network: the model itself. If a tool’s result comes back truncated, oddly worded, or just doesn’t clearly confirm success, a model can decide to call the tool again rather than report an uncertain outcome to the user. That’s a reasonable thing for an agent to do with a read tool. With a write tool, it’s the same failure mode as a network retry, just triggered by ambiguous output instead of a dropped connection — one more reason the tool description matters, a point covered in how to write MCP tool descriptions.
Designing tools that survive being called twice
The fix isn’t telling clients to retry less — retries are correct behavior given an uncertain network. The fix is making the tool itself safe to call twice. A few patterns that actually work, roughly in order of how much they cost to build:
- Accept a caller-supplied idempotency key. Add an optional
idempotency_key(or reuse one your underlying API already supports, as Stripe and many payment APIs do) as a tool input. The server stores which keys it has already processed and returns the original result for a repeat, instead of doing the action again. This is the same mechanism REST APIs have used for years, just carried into the tool schema. - Make the operation naturally idempotent. An
upsert(create-or-update by a stable identifier) doesn’t care how many times you call it; the result converges to the same state either way. Where the underlying API supports it, this is simpler than key tracking and has no failure mode to get wrong. - Check before you act. A
send_invitetool that first checks whether an invite to that email already exists, and returns the existing one instead of sending a second, absorbs the retry without needing the caller to cooperate at all. It costs one extra read before the write. - When none of that is possible, say so in the description. Some actions genuinely can’t be made idempotent — you can’t un-send a text message. For those, the description is the only lever left: state plainly that repeated calls cause repeated effects, and consider requiring an explicit confirmation argument rather than letting a bare retry through unchallenged.
Where this overlaps with security, not just correctness
A tool that isn’t idempotent is also a bigger target once anything untrusted enters the picture. If a model is acting on content it read at runtime — an email, a ticket, a scraped page — and that content nudges it toward calling a write tool again “just to be sure,” you’re looking at a milder version of the same pattern behind the confused deputy problem: a properly authorized tool, called more times than the situation actually warranted, doing real damage without anything technically going wrong. It’s one more reason to keep consequential, hard-to-reverse tools — sending, charging, deleting — behind an approval step rather than letting them run unattended, and to lean on drift-free, plain-language logging so a duplicate call is something you can actually notice after the fact.
The bottom line
Retries aren’t an edge case in an MCP integration — they’re a normal part of how an agent handles an uncertain network or an unclear result, and they will happen to your server whether you planned for them or not. Read tools get this for free. Write tools don’t, and the fix has to live in the tool, not in hoping the client calls it exactly once. An idempotency key, an upsert instead of an insert, or a check-before-you-act pattern each cost a small amount of extra design work up front and remove an entire class of silent duplicate action later.