To prevent duplicate bookings when an agent retries an action, give each intended operation a stable identifier and enforce its meaning in the service that performs the action. Repeated attempts with that identifier should resolve to the same operation. Changed inputs, concurrent attempts, and unknown outcomes need explicit handling; a timeout alone is not proof that nothing happened.
This guide develops a fictional meeting-room booking workflow. Its proposed contract and examples are design recommendations, not results from a customer deployment. The same questions apply when an assistant creates an order, starts an export, or submits a service request.
Start with the missing response
Imagine a user asks an assistant to book Room Cedar from 14:00 to 15:00. The booking service creates reservation B-1042, but the response is lost before the assistant receives it. A second call to an ordinary create_booking action could create a second reservation or trigger another confirmation message.
The service needs a way to recognize the second call as another attempt at the original operation. Simply telling the assistant to “avoid duplicates” does not establish that relationship across network failures or process restarts.
AWS describes this problem in its discussion of making retries safe with idempotent APIs. Its approach uses a caller-provided request identifier to express intent, because matching parameters alone cannot reliably distinguish a retry from a genuinely new request.
For our room service, two bookings with identical details might be an error, but two identical merchandise orders could be intentional. Define what counts as the same operation before choosing how to store keys.
Separate the logical operation from each call
Our proposed booking tool accepts an operation identifier alongside the booking details:
{
"operation_id": "op_demo_cedar_01",
"room_id": "room_cedar",
"starts_at": "2026-10-01T14:00:00+02:00",
"ends_at": "2026-10-01T15:00:00+02:00"
}
These are fictional values, and the readable demonstration identifier is not a production key-generation recommendation. In production, use a suitably unique identifier without embedding personal information.
The operation identifier belongs to the user's one intended booking. A retry keeps it. A separately authorized new booking receives a new one. A transport request identifier can change between attempts without changing the business operation being retried.
We recommend that the application orchestrating the workflow create and persist this identifier before the first attempt. Do not depend on a language model reproducing a randomly invented key from memory. If a server issues the identifier during a preparation step, make its later execution and recovery contract equally explicit.
Tie the operation to the authenticated caller and organization. Possession of an identifier must not grant access to another user's stored result. The access rules described in our remote MCP authorization guide still apply to retries and status lookups.
Write the retry contract before implementing it
For this example, use the following rules:
- New identifier, valid request: register the operation and begin creating the booking.
- Existing identifier, same relevant inputs, completed operation: return the existing booking reference without creating another booking.
- Existing identifier, same inputs, operation still running: return a pending state or a documented retryable conflict; do not start a second execution.
- Existing identifier, different relevant inputs: reject the mismatch and explain that the existing operation cannot be redefined.
- Outcome not yet known: preserve that uncertainty and use a reconciliation path before allowing another attempt to create a booking.
Decide which normalized fields define the input comparison. For the room example, include room, start time, end time, and any other value that changes the reservation. Scope the identifier to the organization and action, and check current authorization before returning a prior result.
A real API illustrates why those details matter. Stripe stores the status and response for an executed request under its idempotency key, including error responses, and rejects reuse with different parameters. Its documentation also distinguishes validation and concurrent-request conflicts from executed requests whose results are saved. These are Stripe's rules, not universal behaviour for every API. See Stripe's idempotent request documentation.
If your MCP tool wraps an existing API, document that API's actual behaviour rather than assuming it matches this example.
Protect the operation at the point of execution
A check followed by a separate insert is insufficient when two attempts arrive together: both can observe that the key is absent. The implementation needs an atomic way to claim the operation, such as a database uniqueness constraint with an appropriate transaction.
For a booking stored in the same database, a design review should establish whether the operation record and reservation can be committed together. If the process crashes after creating the reservation but before recording completion, recovery must still find the reservation instead of creating another one.
External services make this harder. Suppose the tool calls a separate booking provider. Persist the association between the local operation and the provider's idempotency key before sending the request, and reuse that provider key for retries within its supported contract. Where the provider offers no such protection, use an authoritative lookup or reconciliation process. Do not claim safe automatic retries if the remote outcome cannot be established.
Review secondary effects too. A single reservation with three confirmation emails may still violate the user's expectation. Give notifications and other follow-up work their own durable deduplication strategy, or document why repeating them is acceptable. This is an implementation responsibility beyond adding a key to the tool schema.
Decide what happens after the retry window
Idempotency records need a retention policy. Stripe, for example, permits removing keys after they are at least 24 hours old; reuse after pruning creates a new request. That is a concrete reason to check a provider's retention rules before designing delayed retries. See the same Stripe reference.
For our booking workflow, choose a retention period based on expected reconnects, queued work, and recovery needs. State what happens when a caller returns after that period. A durable booking reference or operation lookup may be needed even after the short-term response cache expires.
Avoid converting “the key is no longer stored” into “the booking never existed.” If the outcome is uncertain, tell the caller that the service is checking it. A fresh identifier must represent a new intent, not an automatic escape from uncertainty about the first attempt.
Describe idempotency honestly in MCP
MCP's idempotentHint describes a tool whose repeated calls with the same arguments have no additional effect on its environment. It is meaningful for tools that are not read-only, and it is an annotation rather than an enforcement mechanism. See the MCP ToolAnnotations reference.
Set annotations to match the complete tool behaviour you actually provide. A short-lived deduplication cache may leave important limitations that a broad idempotency claim would obscure. Review the retention and downstream behaviour before advertising the hint.
In the tool description, explain that retries reuse the operation identifier, changed booking details require a separate decision, and pending results should be checked through the documented recovery route. Our tool description guide covers how to communicate boundaries clearly. Wording supports correct use; the backend must uphold the contract regardless of how the caller behaves.
Test the failures that produce duplicates
Use synthetic bookings and verify persistent effects, not just returned messages. For the proposed contract, the release tests should cover:
- A successful first call followed by an identical retry: one booking and the same booking reference.
- A lost response after the reservation is committed: recovery returns the existing reservation.
- Two simultaneous calls with the same identifier: at most one booking is created.
- Reuse of an identifier with a different time: a mismatch response with no second reservation.
- A restart while an operation is pending: recovery does not blindly create a new booking.
- Another organization requests the stored result: no unauthorized result is disclosed.
- A retry after key expiry: the documented late-recovery policy applies.
- Repeated delivery of follow-up work: confirmation handling meets its stated duplication policy.
Make the expected booking count and notification count explicit for each scenario. The test suite should be able to detect a duplicate even when both attempts return plausible success messages.
Scope the first write action around a recoverable outcome
Before exposing a booking or order action to an assistant, identify who owns the operation identifier, where execution is deduplicated, how long the guarantee lasts, and how an uncertain result is resolved. If those answers are missing, the action is not ready for automatic retries.
Start with one workflow whose effects you can trace and test. If you are still selecting that workflow, the Agent Readiness Audit can help frame proposed tools from your API documentation. Confirming retry behaviour requires implementation and failure testing beyond the documentation review.
