MCP Authorization: Tools Are Not Authority
MCP authorization for enterprise agents: bind identity, delegated authority, policy, approvals, execution and signed evidence to every consequential tool call.

MCP authorization begins where tool discovery ends. An MCP server can publish a tool named issue_refund with a clean schema and excellent documentation. A capable agent can discover it, produce valid arguments and call it. None of that answers the question an enterprise must answer before money moves: is this agent allowed to refund this order, for this amount, on behalf of this business function, right now?
Tool discovery and tool authorization are different systems. Treating them as one creates a dangerous shortcut. Authentication proves which credential reached the server. A tool schema describes valid input. Neither proves that an accountable principal delegated this particular action.
This article develops an authorization boundary around MCP. The examples use a refund tool, but the same structure applies to procurement, cloud remediation, claims handling, customer records and any operation with material consequences.
MCP authorization needs four answers for every tool call
Consider this request:
{
"name": "issue_refund",
"arguments": {
"order_id": "ord_8421",
"amount": "480.00",
"currency": "EUR",
"reason": "duplicate_charge"
}
}
A production service needs four separate answers.
- Who is calling? Resolve the runtime credential to an agent and its accountable organisation.
- What was delegated? Find a current grant covering the action, resource, purpose, amount and relevant counterparty.
- Does policy permit this transaction? Evaluate business rules, approval thresholds, separation of duties and current risk state.
- What happened? Bind the decision and execution result to the exact request so another party can verify the record later.
Many MCP deployments answer only the first question. A bearer token identifies a client, then the server maps that client to a broad role. This is familiar and sometimes sufficient for low-risk reads. It becomes brittle when agents operate across several workflows, spawn subagents, retry work and choose tool arguments dynamically.
The fix is not to put more judgment into the model prompt. Authorization belongs at the tool boundary, where the server can reject or constrain the call regardless of what the model says.
Start with identity, then refuse to stop there
OAuth, mTLS, SPIFFE identities and signed tokens are useful foundations. They can establish that a request came from a known workload. They do not explain why the workload is making this request.
Suppose an OAuth client belongs to customer-operations-agent. That agent may have several legitimate jobs:
- read a case to summarize it;
- draft a proposed resolution;
- refund small duplicate charges;
- route larger refunds to a supervisor.
A role such as refund_agent compresses these distinctions into a label. Once assigned, it tends to become standing authority. The label rarely expresses the order, case, purpose, maximum amount, expiry or destination that should constrain the operation.
Keep workload identity, organisational accountability and delegated authority as separate records. The runtime proof authenticates the current caller. An agent passport or comparable identity document links it to an organisation. A short-lived mandate states what a principal delegated for a bounded task.
That mandate could look like this. The example is illustrative, not a copy of an MCP protocol object:
{
"id": "mandate:refund:case-773",
"subject": "agent:customer-ops:17",
"issuer": "org:retailer:customer-care",
"purpose": "resolve_duplicate_charge",
"actions": ["orders.read", "refunds.create"],
"resources": ["order:ord_8421"],
"constraints": {
"currency": "EUR",
"max_amount": "500.00",
"destinations": ["original_payment_method"],
"max_uses": 1
},
"not_before": "2026-08-10T10:00:00Z",
"expires_at": "2026-08-10T10:15:00Z"
}
The tool description still tells the model how to call issue_refund. The mandate tells the enforcement layer whether this instance of the agent has authority for the proposed call.
Put an authorization envelope around the MCP request
MCP should remain the tool protocol. The authorization layer should normalize a consequential call into a transaction envelope before execution. Normalization prevents each tool handler from inventing its own security semantics.
type ToolTransaction = {
transactionId: string
agentId: string
organisationId: string
mandateId: string
tool: string
action: string
resource: string
purpose: string
audience: string
requestDigest: string
issuedAt: string
nonce: string
}
async function authorizeMcpCall(call: ToolCall, context: RuntimeContext) {
const identity = await verifyRuntimeProof(context.proof)
const mandate = await resolveMandate(context.mandateId)
const tx = normalizeToolCall(call, identity, mandate)
verifySignature(context.signedEnvelope, tx)
verifyAudienceAndTime(tx, context.expectedAudience)
const receivedDigest = digestMcpCall(call)
assert(receivedDigest === tx.requestDigest)
await replayStore.claim(tx.transactionId, tx.nonce)
assertMandateCoversTransaction(mandate, tx)
return policy.evaluate({ identity, mandate, transaction: tx })
}
The requestDigest matters. Compute it over a canonical representation of the security-relevant arguments. If a proxy, model or application changes the order ID, amount, destination or reason after authorization, the digest no longer matches. A signature over a vague statement such as "call refund tool" is not enough.
Canonicalization must be specified, tested and identical across languages. Ordinary JSON serialization is risky because key ordering, number formatting and Unicode handling can differ. OATI's developer-preview profile uses RFC 8785 JSON Canonicalization Scheme payloads and shared conformance vectors for this reason.
Evaluate constraints as data, not prose
Do not ask a language model whether the mandate permits the refund. The answer must be deterministic and repeatable.
function evaluateRefund(m: Mandate, call: RefundCall): Decision {
if (m.status !== "active") return deny("mandate_inactive")
if (Date.now() >= Date.parse(m.expiresAt)) return deny("mandate_expired")
if (!m.actions.includes("refunds.create")) return deny("action_not_allowed")
if (!m.resources.includes(`order:${call.orderId}`)) return deny("resource_not_allowed")
if (call.currency !== m.constraints.currency) return deny("currency_mismatch")
if (decimal(call.amount).gt(decimal(m.constraints.maxAmount))) {
return requireApproval("amount_above_autonomous_limit")
}
if (call.destination !== "original_payment_method") {
return deny("destination_not_allowed")
}
return allow()
}
Use decimal arithmetic for money. Check current status and revocation, not merely document expiry. Claim usage and replay state atomically before execution. If the decision depends on a business policy bundle, record its digest and version.
The OATI Decision schema can represent allow, deny, transform and approval_required, while the current reference evaluator emits allow or deny. A production orchestrator can add transformation and approval handling around that evaluator. A transform can remove optional fields or lower a requested limit. It must never silently change the business meaning of the transaction.
Keep credentials out of the model's reach
Even a well-authorized agent should not receive a long-lived payment or service credential in its context. The gateway can obtain a short-lived credential after the decision, bind it to the target service where possible and inject it into the outbound request.
MCP client
-> MCP authorization gateway
-> identity and mandate verification
-> deterministic policy decision
-> temporary credential broker
-> existing refund API
-> response filter
-> signed receipt
This design narrows the cost of prompt injection. When every consequential path passes through enforcement, malicious text may persuade the model to attempt an unauthorized call, but the model should not be able to mint authority, widen policy or reveal a credential it never receives. Bypass paths and broker misconfiguration still need separate tests.
Bind evidence to the observed execution
The service should record both permitted and denied attempts. For an allowed call, the receipt should connect the agent, organisation, mandate, transaction, decision and observed outcome.
{
"id": "receipt:refund:tx-9912",
"transaction_id": "tx:refund:9912",
"agent_id": "agent:customer-ops:17",
"organisation_id": "org:retailer",
"mandate_id": "mandate:refund:case-773",
"decision": "allow",
"outcome": "succeeded",
"request_digest": "sha256:...",
"policy_digest": "sha256:...",
"external_reference": "refund_provider:rf_317",
"occurred_at": "2026-08-10T10:04:31Z"
}
Sign the canonical receipt and retain enough evidence to verify its references. Be precise about what it proves. The signature proves what the issuer attested about its record and control path. It does not prove that no bypass existed, that the customer's claim was truthful or that the payment provider's external record is correct. Reconciliation and dispute procedures still matter.
Choose the enforcement point you can defend
There are three practical places to enforce this design. An MCP server can embed verification directly in each sensitive handler. This keeps the decision close to execution, but duplicated libraries and policy configuration can drift across servers. A shared MCP gateway can intercept calls before they reach existing servers. That centralizes normalization and policy, although it becomes a high-value component that needs tenant isolation, replay protection and careful availability engineering. A service can also enforce at its ordinary HTTP or gRPC API after the MCP server translates the call. This protects every caller, not only MCP clients, but requires the translation layer to preserve the signed transaction context.
For a material action, use defense in depth. Let the gateway verify the caller and reserve usage, then let the service confirm the transaction digest and decision before committing the operation. Avoid two independent policy implementations. The service should verify an authenticated decision artifact or call the same deterministic evaluator with the same signed policy bundle.
Local evaluation also changes the outage story. A customer-side gateway can continue with signed policy bundles and fresh revocation state when the control plane is unreachable. High-risk writes should fail when trust or replay state is too old to support a defensible decision. Explicitly configured low-risk reads may use a different rule. The classification belongs in policy, not in a generic "fail open" switch.
Observability must preserve the same boundary. Send transaction IDs, decision reasons, policy versions and timings to central telemetry. Keep raw prompts, tool payloads and secrets in the customer environment unless the customer has approved their release. Debugging convenience is not a reason to turn the authorization plane into a copy of sensitive business data.
Threats your happy-path demo will miss
Mandate substitution. The caller attaches a valid mandate for another case. Bind the mandate ID, subject, action, resource and purpose to the transaction.
Argument mutation. Middleware authorizes EUR 48, then a downstream component submits EUR 480. Bind the canonical request digest and verify it at the final execution boundary.
Audience confusion. A signed request intended for a test server is accepted by production. Require the expected audience and reject reusable proofs.
Replay. A valid one-time refund call is submitted twice. Atomically claim the transaction ID, nonce, mandate usage and provider idempotency key.
Stale authority. A cached mandate remains accepted after emergency revocation. Define cache age, revocation lookup behavior and fail-closed rules for material actions.
Tool aliasing. Two MCP servers publish similar tool names with different effects. Authorize a stable action and resource identifier, not only the display name exposed to the model.
Confused deputy. A low-privilege agent asks a privileged tool server to use its own broad credential. Require the caller's delegated authority even when the server possesses execution credentials.
Output leakage. The action is authorized, but the response contains fields the agent may not receive. Apply output policy and destination controls after execution.
An implementation checklist
- Map every runtime credential to a stable agent and accountable organisation.
- Separate capability discovery from transaction-specific authority.
- Define stable action and resource identifiers for consequential tools.
- Normalize MCP calls into a canonical transaction envelope.
- Bind signatures to audience, time, nonce and the exact request digest.
- Resolve issuer, key, status and revocation before policy evaluation.
- Evaluate mandates and business policy with deterministic code.
- Claim replay, usage and budget state atomically.
- Route exceptions through an approval that references the exact transaction.
- Broker short-lived credentials after authorization.
- Filter responses under destination and data-use rules.
- Sign receipts and retain correlation data for reconciliation.
- Test substitution, mutation, replay, stale-cache and partial-failure cases.
Related engineering guides
- Follow the full lifecycle in AI Agent Architecture for Auditable MCP Transactions.
- Test the boundary against replay and mandate-substitution attacks.
Where OATI fits today
OATI models this boundary with Passport, Mandate, Transaction Envelope, Decision and Receipt objects. The public developer-preview framework includes JSON Schemas, canonical JSON, signature verification, deterministic mandate evaluation, lookup and discovery, TypeScript middleware and shared conformance vectors across TypeScript, Python and Go. The published suite currently contains 73 language-neutral cases.
That status should not be mistaken for a completed enterprise product. The policy compiler and durable evidence and dispute worker remain incomplete, and the customer gateway is a reference integration rather than a fully operated commercial fleet. Independent cryptographic and protocol review is still an open release gate.
The useful architectural point does not depend on adopting OATI: let MCP describe and carry tool calls, but require an independent authorization layer to decide whether a particular agent may perform a particular transaction. A model can propose the call. It should never be the component that grants itself permission.