Idempotency
Write operations (POST, PUT, PATCH) accept anIdempotency-Key header to
prevent duplicate actions when agents or integrations retry requests.
This page covers two surfaces with different semantics: the REST middleware
(below) and MCP tool calls. Read the MCP section before
wiring retries into an MCP client — the failure modes are not the same.
How It Works
- Generate a unique key (UUID recommended) for each logical operation
- Send it in the
Idempotency-Keyheader - If the same key is sent again within 24 hours with the same body, the original response is replayed without re-executing the operation
- If the same key is sent with a different body, you get
409 Conflict
Example
Behavior
Key Requirements
- Maximum 255 characters. This is the only length rule the server enforces,
and it applies to both surfaces — but they fail differently:
- REST rejects a longer key outright with
400 IDEMPOTENCY_KEY_TOO_LONGbefore your request reaches the handler. - MCP does not error. A key over 255 characters silently disables idempotency for that call: the tool runs, nothing is cached, and a retry executes the mutation a second time. Validate key length client-side.
- REST rejects a longer key outright with
- No minimum length is enforced. Short keys are accepted, but a key needs enough
entropy to be unique per logical operation — UUID v4 is the recommendation, not
a requirement:
crypto.randomUUID(). - Must be unique per logical operation.
Best Practices
- Always include
Idempotency-Keyon mutation endpoints - Store the key alongside your operation log for debugging
- Use a deterministic key (e.g.,
${orderId}-assign-${staffId}) when retrying the same logical operation
Idempotency over MCP
The REST middleware keys off HTTP method + body hash, so it never engages for MCP — every MCP call is the samePOST /mcp, and the real intent (tool name +
arguments) lives in the JSON-RPC body. MCP therefore has its own implementation,
sharing the same 24-hour store but with different rules.
Supplying the key
Two ways, and_meta wins when both are present:
params._meta.idempotencyKeyon thetools/callrequest (preferred — it travels with the call, not the transport).- The
Idempotency-KeyHTTP header onPOST /mcp.
When it engages
Only for mutating calls: a named verb markedmutating, or a registry tool
that is not read-only. Read tools ignore the key entirely and never cache — no
error, no warning. Sending a key on a read is harmless but pointless.
Keys are namespaced per credential and per tool
The stored key ismcp:{k<apiKeyId>|u<userId>}:{toolName}:{yourKey}. Two
consequences:
- Your key can never collide with another caller’s.
- The same key on a different tool is a different operation. Reusing one key
across
fillShiftandswapShiftswill not deduplicate them, and will not conflict either. Scope your keys to the logical operation, not to the retry loop.
The five states
Both
-32009 bodies use the same code, so data.retryable is the only field
that distinguishes them. Branch on it.
The reservation is inserted before the tool runs, so two concurrent calls
with the same key can never both execute. If that reservation insert fails for
any reason other than losing the race, the call fails closed and answers
in_progress — the server would rather make you retry than risk a
double-execution.Errors and approvals release the key
Two behaviors that make the retry story work:- Tool errors are never cached. If the call comes back with
isError, the reservation is dropped. Retrying with the same key re-executes — you are not stuck replaying a failure for 24 hours. pending_approvalreleases the reservation. When a governed write pauses for human approval, the key is freed. Once a human approves, retry with the same key — that is the supported flow, and it is why the reservation is not held. (Without this the key would answerin_progressuntil its TTL expired, permanently wedging the call → approve → retry loop.)

