An approved MCP tool call often fails once: the downstream API times out, the transport drops, or the agent runtime remaps the same intent onto a slightly different tool. The dangerous default is to treat the first approval as reusable for anything that looks like a retry.
Exact-args approval is not a blank check. Permissions still decide what the identity can do. Approval still decides whether this exact permitted action may proceed now. A prompt that says "ask before writing" does not enforce either control, and a successful review does not automatically authorize a second, wider, or remapped call.
This post is about the controls that must survive retries and remaps. For the broader permission and approval design, see how to scope AI agent permissions and human approval. For server-side production controls around MCP itself, see how to secure an MCP server for production.
Permissions and approval stay separate under retry pressure
Permissions answer what the service identity is allowed to do at all. Approval answers whether one concrete, already-permitted operation may run in this moment.
That split still holds when a run retries:
- A permission check can pass for a remapped tool while the original approval does not apply.
- An approval can remain valid for the exact args while the permission check fails because tenant scope, token audience, or policy changed while the request waited.
- A prompt or sticky "always allow this tool" preference is still not access control.
If your runtime collapses "the model wanted to send something" into "reuse the earlier yes," you have built an elevation path. The retry path must re-evaluate identity, policy, approval binding, and current resource state as independent gates.
Narrow tools before you invent smarter retries
Retries amplify whatever tool surface you expose. A generic execute-any-API tool turns one timeout into an unbounded second attempt. Prefer purpose-built tools with explicit argument schemas.
Useful defaults for MCP servers that call business systems:
- separate prepare or preview tools from execute tools;
- give each workflow its own service identity and tenant scope;
- validate tenant, allowed fields, and current state in deterministic code at execution time;
- reject argument shapes the schema never intended, even when a human already reviewed a similar call.
The official MCP authorization tutorial emphasizes least-privilege scopes per capability, token validation, short-lived tokens, and correct audience checks. The MCP security best practices make the server responsible for authorizing every inbound request and forbid token passthrough. Those rules do not disappear because the client is retrying an earlier call.
Bind approval to immutable arguments
An approval should authorize one exact operation. Canonicalize the tool name and arguments, store a hash with the approval record, and reject execution when the payload drifts. Bind the authenticated requester, tenant, target resource version, and an expiration time into the same record.
Single-use is the right default. If policy allows a sticky approval for a low-risk read tool inside one tightly scoped run, treat that as an explicit policy decision with its own expiry and audit trail, not as a convenience toggle that silently covers writes.
Practical binding rules:
- canonicalize key order, string encoding, and null handling before hashing;
- include the tool name in the hash so a rename or remap cannot inherit consent;
- store the resource version or optimistic concurrency token the reviewer saw;
- expire the approval on a short clock appropriate to the risk;
- consume the approval token when execution starts, not only when it succeeds.
If the agent edits recipients, amount, fields, attachments, or the resolved record set after approval, that is a new proposal. Create a new approval version and re-run validation. Do not patch the old decision in place.
A wider or remapped retry is not the first approval
Runtimes often remap after a failure: a payment tool becomes a "generic HTTP POST," a narrow CRM update becomes a bulk write, or a prepare step is skipped and execute is called with reconstructed args. Those are not retries of the approved call. They are new tool invocations.
Controls that should reject the remap:
- tool-name equality against the approved binding;
- argument-hash equality against the approved binding;
- permission checks for the remapped tool, which may be broader or unavailable;
- policy checks for impact class (message send, money move, bulk change, delete);
- resource-version checks against current state.
Revalidate every gate immediately before execute, even when the hash still matches. Permissions, policy, and external state can change while a human was reviewing or while a queue sat idle. Approval does not freeze the world; it only freezes the decision about one payload for a limited time.
Persist an idempotency key before the outbound call
Crash and timeout recovery needs a durable key that exists before any side-effecting request leaves your boundary. If you generate the key only after a successful response, a process that dies mid-flight will invent a new key on restart and duplicate the effect.
A sound sequence for an approved write: 1. Validate schema, tenant, policy, and current resource version. 2. Confirm the approval binding still matches (tool, hash, requester, tenant, expiry, single-use token). 3. Persist an idempotency key and the intended payload in your own store, marked pending. 4. Call the downstream API with that key (or the provider's equivalent header) and record the attempt. 5. On success, store the external operation ID and mark the approval consumed. 6. On timeout or ambiguous failure, reconcile with the provider using the same key before starting a new attempt.
If the provider does not support idempotency keys, keep your own ledger of intended effects and reconcile by natural business keys before retrying. The webhooks vs polling guide covers related reconcile-before-retry patterns for inbound provider events; the same discipline applies to outbound agent actions.
Preventing duplicate side effects after a crash is an execution concern, not a model concern. The agent may re-propose the same intent. Your store decides whether that intent already ran.
Retry scenarios and the controls each one needs
Build the matrix from real tools and providers, not from the phrase "retry safely." The rows below show the level of specificity required.
| Retry scenario | What often goes wrong | Required controls before execute |
|---|---|---|
| Same tool, identical args, after timeout | Duplicate charge, message, or write | Approval still bound and unconsumed; persisted idempotency key; reconcile provider state; recheck resource version |
| Same tool, args drifted (recipient, amount, fields) | First approval used for a different effect | Reject on payload-hash mismatch; new approval version; full validation restart |
| Remapped to a wider tool | Narrow consent becomes open-ended API access | Reject tool-name mismatch; no approval inheritance; separate permission check for the wider tool |
| Remapped to a different narrow tool with similar intent | Silent policy bypass across tool boundaries | Treat as a new proposal; bind approval to the new tool name and args |
| Prepare succeeded, execute retried after crash | Second execute after first already committed | Idempotency ledger for execute; consume approval on first start; reconcile before second execute |
| Sticky "always allow" for a write tool | One review covers every future write | Sticky approvals only for low-risk reads by policy; writes stay single-use with expiry |
| Approval expired while queue waited | Stale yes applied to current state | Enforce expiry; re-request approval; revalidate permissions and resource version |
| Permission revoked mid-flight | Former identity still executes | Recheck identity, scopes, and tenant on every attempt; do not cache authorization across retries |
The important column is not "retry." It is which immutable material must still match and which gates must run again.
What to show the reviewer when a retry needs a fresh decision
If the payload changed, the reviewer needs the same quality of surface as the first review: action and tool name, destination or record, before-and-after fields, external content, amount or affected count, reversibility, and why approval is required again. Say clearly that a previous approval does not apply because the arguments, tool, or resource version changed.
Do not ask the reviewer to approve "retry payment" as a vague intention. Ask them to approve the exact execute call that will run. Keep raw JSON available for technical review, but put the material facts in plain language first.
Audit the retry path, not only the first yes
An audit record should connect the original request, each proposed tool call, each approval version, each retry attempt, the idempotency key, reconcile outcomes, and the external operation ID. Record rejected remaps and expired approvals. Those denials are how you prove the binding worked.
Keep sensitive payloads in an access-controlled audit store. A retry log that every engineer can read can become a new exposure even when the action itself was correctly gated.
Put the controls in the server and the execution service
Client-side consent screens and model instructions help operators, but they are not the enforcement point. The MCP server and the narrow execution service must:
- authorize every inbound tools/call;
- enforce approval binding and single-use semantics;
- own the idempotency ledger;
- refuse remapped or drifted payloads;
- revalidate permissions and resource state immediately before side effects.
For operational runtime hygiene around self-hosted agents, use the self-hosted AI agent deployment checklist. For implementation work, custom MCP server development applies these checks at the tool boundary, AI agent systems design approval into the run, and systems integration covers provider idempotency and reconcile patterns.
The design goal is narrow: the first approval authorizes one exact permitted call. Retries that match that call still revalidate and stay idempotent. Remaps and wider tools start over.
