Months ago, a commenter on this series proposed a test: if the same backend really stands behind both doors, then auth, error behavior, and idempotency must be preserved across them. Authorization Stays in the Kitchen settled the first leg. The confusion telemetry and recovery messages settled the second. This post settles the third, and it turns out to be the leg that matters most for agents, because agents retry.
Why agents make this urgent
Code retries when a developer writes a retry policy. Agents retry on their own judgment: a timeout, an ambiguous error, a truncated response, and the model decides the call probably failed and tries again. Nothing in the MCP protocol stops the second call from being a second order. The new driver in the companion repo proves the failure mode as a control case before proving the fix:
3. retry without clientOrderId PASS: orderId 9714f45b then 99523aed (two orders exist, as feared)
Same restaurant, same item, same quantity, two calls, two orders. Every production incident involving an agent and a non-idempotent write looks like this line.
One decision, one place, again
The series rule holds: neither door contains business logic, and deduplication is business logic. The kitchen keeps a store keyed on the caller's idempotency key, remembers a fingerprint of the request alongside the original confirmation, and answers one of four ways: placed, replayed, conflict, or invalid.
if (idempotencyKey is not null
&& _byIdempotencyKey.TryGetValue(idempotencyKey, out var seen))
{
return seen.Fingerprint == Fingerprint(request)
? new OrderResult(OrderOutcome.Replayed, seen.Confirmation)
: new OrderResult(OrderOutcome.Conflict, null);
}
A replayed key with the same request returns the original confirmation and creates nothing. The same key with a different request is a conflict, which is always a caller bug and deserves to be refused loudly. The doors only translate those outcomes into their caller's dialect, exactly as they did for authorization.
The REST door speaks header
HTTP already has a convention for this: the Idempotency-Key request header, specified in an IETF httpapi draft that Stripe, Adyen, and most payment APIs shaped in practice. The draft says a completed operation replays its original result and a key reused with a different payload SHOULD get a 422, so that is what the door does.
Verified live, the same curl twice:
HTTP/1.1 200 OK
{"orderId":"97e92454","restaurantName":"Trattoria Valkhof","itemName":"Margherita","quantity":2,"total":23.00,"status":"confirmed"}
HTTP/1.1 200 OK
{"orderId":"97e92454","restaurantName":"Trattoria Valkhof","itemName":"Margherita","quantity":2,"total":23.00,"status":"confirmed"}
Same orderId, one order, and the client cannot tell the replay from the original, which is the whole point. Then the same key with quantity 5:
HTTP/1.1 422 Unprocessable Entity
{"error":"idempotency_conflict","detail":"This Idempotency-Key was already used for a different request. Send a new key for a new order."}
The MCP door has no headers
Here the two doors genuinely differ, and the difference is instructive. A tool call carries a name and arguments, nothing else, so the idempotency key cannot ride in a header. It has to be an argument, which means it has to be in the contract, which means a sentence has to teach the agent to use it:
[McpToolProperty("clientOrderId", "An order reference that comes from you, the caller: invent a unique value, and reuse the same value if you retry this order after an error or timeout.", isRequired: false)]
string? clientOrderId
That sentence is the mechanism. On the REST door, idempotency works because a developer read the docs and wrote a header into their client. On the MCP door, it works only if the description convinces the model to invent a reference and hold onto it across a retry. There is no SDK to do it for the agent; the prose is the SDK.
The series tooling priced and policed the sentence immediately. The contract linter flagged clientOrderId as an identifier parameter the moment it existed, so the description had to say where the value comes from, and this parameter gave the provenance rule its first unusual answer: it comes from you. The token budget rule priced the addition at 42 tokens, taking the contract from 302 to 344 against a budget of 350, so the next contract sentence will have to pay for itself by shortening another. And because the deployed description hash is computed from these same attributes, the descHash changed, and the confusion telemetry can attribute any behavior shift to this exact wording.
The driver verified both halves end to end against the deployed server:
1. retry with same clientOrderId PASS: orderId d120204a then d120204a (one order exists)
2. same key, different request PASS: The clientOrderId 'order-2ccc9bd3' was already used for a different order.
The conflict answer is recovery prose, as this door's denials always are, and it goes out through the telemetry helper as a new order_conflict sentinel, so key misuse is observable next to every other contract failure. Replays log their own trace, which gives you a retry rate for free: how often agents actually double-fire is now a query, not a guess.
Honest limits
The store is in-memory, like everything in this sample, so the replay guarantee spans one instance and one process lifetime. A production version needs a durable store with a TTL, and on a plan that scales out, a shared one. The guarantee is also only as good as the agent's discipline: an agent that invents a fresh clientOrderId on every retry gets no protection, and nothing in the protocol forces it to comply. The description can only persuade. That makes idempotency on the MCP door a probabilistic defense in a way it never was for REST clients, and a good candidate for the eval harness to measure someday: given a simulated timeout, does the model actually reuse the key the description told it to reuse?
The takeaway
The parity test now passes on all three legs, and each leg landed the same way: one decision in the kitchen, one dialect per door. For idempotency the dialects are a standard header on one side and a contract sentence on the other, and the asymmetry is the lesson. REST made retries safe with infrastructure; MCP has to make them safe with language. If your tools write anything that costs money, the deduplication store is the easy half. The sentence that gets the agent to use it is the half this series keeps measuring.
The kitchen policy, both doors, and the double-fire driver are in the companion repo; run measure/drive_idempotency.py against your own server and see whether check 3 scares you as much as it should.

Top comments (1)
The control strip in the diagram makes the argument by itself: same retry, no key, two confirmed €23 orders. Agents retry on their own judgment, so idempotency stops being optional the moment an MCP door exists.
One thing worth spelling out for readers: the dedupe store maps the key to a fingerprint of the request, which is why the same key with quantity 5 gets a conflict instead of a silent replay. Many implementations store only the key and return the old result for a changed request, which is arguably worse than no idempotency.
How long do you keep keys? If an agent session resumes after the TTL expires and retries with the same clientOrderId, it gets a fresh order. That's probably the next failure mode after this one.