Skip to main content

Idempotency

Write operations (POST, PUT, PATCH) accept an Idempotency-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

  1. Generate a unique key (UUID recommended) for each logical operation
  2. Send it in the Idempotency-Key header
  3. If the same key is sent again within 24 hours with the same body, the original response is replayed without re-executing the operation
  4. If the same key is sent with a different body, you get 409 Conflict

Example

Sending the exact same request again replays the original response — the staff member is not assigned twice.

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_LONG before 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.
  • 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-Key on 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 same POST /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:
  1. params._meta.idempotencyKey on the tools/call request (preferred — it travels with the call, not the transport).
  2. The Idempotency-Key HTTP header on POST /mcp.

When it engages

Only for mutating calls: a named verb marked mutating, 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 is mcp:{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 fillShift and swapShifts will 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_approval releases 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 answer in_progress until its TTL expired, permanently wedging the call → approve → retry loop.)

TTL

24 hours, same as REST. After that the key is treated as new.
The “deterministic key” advice in Best Practices is written for REST. Over MCP a deterministic key is still fine, but remember it is namespaced by tool name — and that a disabled state (over-long key) gives you no signal that deduplication is not happening.