Human Approval, Idempotency, and Traceable Tool Calls for AI Agents
This design is independent of model vendor. The MCP 2025-06-18 specification recommends keeping a human able to deny tool invocations and warns clients that annotations from untrusted servers are not trustworthy. The deeper principle is that a protocol transports a call; the product decides whether the call is allowed.
Fields such as readOnlyHint, destructiveHint, and idempotentHint are hints. They cannot substitute for an application-owned policy.
Classify risk before exposing tools
Divide tools into reads, reversible writes, and irreversible or external effects. Public-data lookup may run automatically. Creating an internal draft may run with an audit event. Sending a message, transferring funds, deleting, publishing, or changing permissions should require a person by default. Risk also depends on arguments: reading the current user’s file and reading an arbitrary tenant’s file are not the same action.
A policy engine receives the authenticated user, tenant, tool, normalized arguments, resource, and task origin. It returns allow, deny, or require_approval. The model cannot revise that result. Tool descriptions, retrieved text, and MCP server output are all untrusted inputs.
Keep reads and writes as separate registered functions even if an upstream API combines them. The split gives policy a clear surface and makes accidental mutation easier to detect.
Approve at action time
A person may review a plan, but consequential approval occurs after the tool name and normalized arguments are known. The approval view presents the action, target, expected effect, supporting evidence, and reversibility—not only a model-written summary.
Bind the approval to a call id, argument hash, approver, timestamp, and expiry. Any changed destination, amount, attachment, or permission invalidates it. A batch approval must constrain action type, target set, count, and time window. “Approve this agent” must never mean approval of all future behavior.
An emergency stop prevents calls that have not started and revokes short-lived credentials. For an in-flight external request, record the uncertain state and reconcile; do not promise cancellation that the downstream service cannot provide.
Idempotency is a business contract
Model retries, network timeouts, and worker crashes can all duplicate a request. Generate a stable business idempotency key for every write, such as task_id + step + normalized_target. The executor reserves the key in a database before calling the external system. Recovery consults the reservation and external facts rather than asking the model whether it remembers completion.
Idempotency is more than an HTTP header. When a downstream service lacks native keys, use a local uniqueness constraint and reconciliation workflow. Persist partial success instead of rerunning a whole batch. A transient read may retry within policy; authorization denials and business conflicts should not.
Define the result of duplicate submission explicitly. Returning the original result is usually more useful than a generic conflict, but the stored result must be bound to the same normalized operation.
Keep an append-only execution trail
Append events as a call moves through proposed, validated, waiting_approval, approved, started, succeeded, or failed. Include model and prompt versions, tool schema version, redacted arguments, policy decision, approver, idempotency key, result summary, and trace id. Store sensitive values by secure reference or hash, not in ordinary logs.
Distinguish protocol failure, tool-level business error, policy denial, timeout, and unknown external state. Unknown is the dangerous case: reconcile it instead of immediately retrying. The trail must answer who authorized what, how many executions occurred, and what the external system reports.
Put authority in a default-deny gateway
Execute only registered tools. Apply schema validation first and domain authorization second. Give each task short-lived, least-privilege credentials. Bind access-token audience to the intended resource and never pass a client token unchanged to a downstream API. Independently allowlist outbound network destinations, filesystem roots, and database roles.
Bound the loop by steps, time, spend, and repeated failures. Cancellation, approval expiry, or user revocation causes the gateway to reject subsequent calls. The final model message is not the source of truth; committed tool results and the external system are.
Test the failure paths
Exercise duplicate submission, arguments changed after approval, a worker crash after external success, forged tool annotations, cross-tenant ids, prompt injection, expired approval, and cancellation races. Assertions should cover more than task completion: zero unapproved effects, at most one effect per idempotency key, and an explainable state for every attempt.
A reliable agent is not primarily a longer prompt. The model proposes. Policy decides. A person approves high-risk actions. The executor holds authority, while the database owns idempotency and recovery. With those roles separated, failures in the model, MCP server, worker, or network remain bounded and auditable.