Idempotency in MCP
How to make MCP write tool calls safely retryable.
Why idempotency matters even more in MCP
In a normal HTTP integration you control the retry. In an agent integration, the agent may decide to retry on its own — or two parallel agent runs may both try to create the same customer. Idempotency keys are how you stop that from turning into duplicates.
How it works
EPD’s idempotency mechanism is identical to the REST API: the same 24-hour window, the same conflict semantics, the same error codes. The only difference is how you pass the key.
REST
POST /v1/orders
X-EPD-Idempotency-Key: 8e2c1b9a-4f3d-...
MCP
Every write tool accepts an optional idempotency_key argument:
{
"name": "create_order",
"arguments": {
"customer_id": "550e8400-e29b-41d4-a716-446655440000",
"payment_method_id": "6ba7b815-9dad-11d1-80b4-00c04fd430c8",
"items": [
{ "product_id": "6ba7b810-9dad-11d1-80b4-00c04fd430d1", "quantity": 1 }
],
"idempotency_key": "8e2c1b9a-4f3d-..."
}
}
If you don’t pass one, the MCP server generates a key from the call’s content. This dedupes accidental retries from the same session — but it cannot dedupe across sessions or across agents. Pass an explicit key whenever a retry from a different process is possible.
When to pass an explicit key
A user submits a “sign up + first charge” form. Generate an idempotency key once on submit, pass it through to the MCP tool. If your worker crashes and restarts, the same key arrives — no duplicate charge.
Importing 10,000 customers? Per-row idempotency keys make the whole job restartable from where it failed.
If two agent runs may try the same action, share an idempotency key derived from the underlying intent (e.g. an order id from your system).
create_customer_and_charge is one logical action — pass one key. EPD propagates it to the inner steps.
Errors
| Error code | Meaning |
|---|---|
idempotency_key_in_use | A previous call with this key is still running. Retry after a short delay. |
idempotency_key_conflict | Same key, different arguments. Use a fresh key for the new intent. |
invalid_idempotency_key | Key is empty, too long, or has illegal characters. |
Read tools and idempotency
Read-only tools (get_*, list_*) are naturally idempotent and do not require a key. The annotation idempotentHint: true confirms the tool is safe to retry.