An agent calls create_ticket. The ticketing API succeeds, but the MCP connection drops before the result reaches the agent. Should it call the tool again? Without a retry contract, a reasonable-looking retry can create a second ticket. The fix belongs in the tool and its backing service, not in an instruction asking the model to be careful.

This guide focuses on write operations whose outcome may be unknown to the caller. If you are handling the request deadline itself, start with MCP timeouts and cancellation. A timeout does not tell you whether a write committed.

Decide what counts as the same operation

A retry needs a stable identifier supplied by the caller. Add a required operation_id argument to a write tool and tell the client to reuse it for retries of the same intended action. A new ticket, even with identical text, gets a new ID. Keep the identity separate from the transport request ID: a new tools/call may have a new JSON-RPC ID while referring to the same business operation.

For example, an illustrative tool input might be:

{
  "project_id": "support",
  "title": "Login fails after password reset",
  "operation_id": "8b8be44e-92e4-45c7-944b-4081e91f59bf"
}

The UUID is an example, not a value to copy. Generate a new unpredictable ID for each intended write and persist it on the caller side until the outcome is known. This is an application-level contract, not a field that MCP automatically adds to every tool call. Describe it in the tool’s input schema and documentation. Input validation should reject missing or malformed IDs before invoking the downstream API.

Record the outcome where the write happens

On the server, scope an operation ID to the authenticated tenant and tool action. Save a fingerprint of the meaningful input alongside its state and result. A repeat with the same ID and input returns the recorded outcome; the same ID with different input is a conflict, not an instruction to overwrite the first action. Never trust a tenant ID from tool arguments as the authorization boundary.

A useful operation record has states such as pending, succeeded, and failed, plus the resulting ticket ID when known. Put a unique constraint on (tenant, action, operation_id) so two concurrent calls cannot both claim the ID. If the write and operation record live in the same database, commit them in one transaction where practical. Recording a successful ticket after committing it in a separate transaction leaves a gap where a crash can still produce a duplicate.

If the write calls an external service, pass the same idempotency key downstream when that service supports one. Your local database cannot make an unrelated API call and your own record atomic by itself. If the downstream service does not support idempotency, expose an unknown or pending outcome and reconcile against its authoritative records before retrying. Do not claim exactly-once execution when you can only detect some duplicates after the fact.

Make an unknown result legible to the agent

Return a durable business identifier and an explicit status, not just “Done.” A successful response can include operation_id, status: "succeeded", and ticket_id. If a call is still in progress, return pending with instructions to check its status using the same operation ID. If your implementation does not support a status lookup, document that limitation and make a human or backend reconciliation path available for consequential actions.

Keep errors distinguishable. Validation failure means nothing was attempted. A downstream timeout after submission means the result is unknown. An authenticated status check that confirms no action occurred is different from a status check that simply cannot reach the provider. A model should not infer “safe to retry with a fresh ID” from a generic timeout message.

For payments, emails, and deployments, an operation ID can prevent an accidental duplicate caused by a replay. It does not replace user approval for the original side effect. Keep authorization and approval checks on the server, including when a call reuses a key. See production MCP authentication for the separate question of who may call a tool.

Annotate behavior honestly

MCP tool annotations include idempotentHint. It is a hint about behavior, not the implementation of deduplication and not a security guarantee. Mark a write tool idempotent only if repeated calls with the same arguments have the claimed effect under your actual storage and downstream failure modes. A tool that accepts operation_id but forwards each attempt as a fresh email is not retry-safe.

Likewise, do not label a write as read-only because it offers a dry-run mode. If dry run is important, make it a separate read-only preview tool with no side effects; keep the actual write explicit. Tool names and result fields should let a person inspecting an agent trace tell the difference between a proposed action and a committed one.

Test the failure window

Use the local MCP debugging workflow to exercise these cases before connecting an agent to real accounts:

  1. Call the tool twice with the same tenant, operation ID, and payload. Confirm there is one downstream action and one stable result.
  2. Reuse the ID with changed input. Confirm a conflict, with no second write.
  3. Start two identical calls concurrently. Confirm the unique constraint prevents a race.
  4. Drop the response after the downstream action commits. Retry with the original ID and confirm it returns the original outcome.
  5. Try the same ID under another tenant. Confirm it neither reveals the first tenant’s result nor acts under that tenant’s authority.
  6. Simulate a downstream timeout that leaves the result uncertain. Confirm the tool reports uncertainty rather than success or a clean failure.

The strongest test deliberately cuts the connection at the awkward moment, after the side effect but before the result is delivered. A happy-path duplicate test alone does not prove your recovery path works.

FAQ

Does MCP supply an idempotency key automatically? No. Define the key in your tool’s application-level input contract and keep it stable across retries of one intended action.

Can I use the JSON-RPC request ID? Not reliably. It identifies a particular protocol request. A retry may be a new request for the same business action.

Does idempotentHint: true make the tool safe to replay? No. It describes intended behavior. The server and any downstream service must actually enforce that behavior, and an untrusted server’s annotation is not proof.

When should a caller create a new operation ID? When the user intends a new write. If the prior outcome is unknown, check its status or reconcile it first instead of changing the ID to make the retry go through.