Companion release: github.com/aiyoplane/mcp-authz — Apache 2.0. Live demo: mcp-authz.aiyoplane.com.
The problem MCP creates — and hides
The Model Context Protocol makes it easy for an autonomous agent to invoke a tool. Register the tool, describe its inputs, describe what it does, and the agent can call it. MCP has become a major integration surface for agentic systems. The protocol is doing its job.
That job produces a very specific new problem, and the problem is not something MCP itself set out to solve.
Consider a tool named deploy_to_production. The MCP server registers it. The agent, in principle, has permission to invoke it — the tool is in its capability set, and MCP will happily route a tools/call request straight into the tool's handler. But at 3 a.m. on a Saturday during an active incident, with the on-call engineer paged, the correct answer to "should this specific deploy_to_production invocation execute now?" is no — regardless of whether the agent is generally permitted to call the tool. The runtime conditions that determine whether this invocation is authorized are entirely separate from the registration-time permission to call it.
Now consider a tool named charge_customer($5000). Same shape. Same distinction. Permission to invoke ≠ authorization for the consequential action. The tool being registered is about interface. The invocation being authorized is about consequence.
This is not a payment problem. It's the same problem in a different colour. An MCP tool that writes to a CRM, deploys code, executes trades, sends messages, calls another paid API, or does anything else with a real-world consequence lives in exactly the same shape. Permission to invoke says nothing about whether the specific invocation, under the specific runtime conditions right now, is authorized.
The MCP spec doesn't attempt to answer this. It's out of scope by design — MCP is a tool-invocation protocol, not an authorization protocol. But the gap it leaves is precisely the shape of the authorization problem for autonomous systems, and if the answer to that gap is written per-server, per-tool, per-team, by ad-hoc middleware and inline if statements, we get exactly the fragmented authorization landscape agent-security incident reports have been describing all year.
We wanted to see whether a small piece of software — genuinely small, deliberately not a standard — could show what the right shape of the answer looks like.
The question we asked
Given an autonomous agent, an MCP server, a consequential tool, and a merchant policy that names the conditions under which the tool should be permitted to run:
Where in the request lifecycle should an independent authorization decision occur?
Two invariants we set upfront, before writing any code, and revisited at each design decision:
- One authorization decision per consequential action. Not one per session, not one per workflow, not one aggregated across multiple actions. Every consequential invocation is its own authorization event. The decision has to be about this specific action, in this specific runtime context, not about a broader batch.
- The decision must occur strictly between intent and execution. If the RDP evaluates before the intent is knowable, the decision was too early — the concrete parameters of the action weren't yet observable. If it evaluates after the action has already run, the decision was too late — for consequential actions, rollback is not always possible. The evaluation must land in the narrow window where the intent is fully formed and the action has not yet begun.
Any candidate composition point had to satisfy both.
We enumerated four places you could hang the decision, and evaluated each against the invariants:
| Composition point | Invariant 1 | Invariant 2 | Verdict |
|---|---|---|---|
| Session-level authorization (authorize at agent connect; execute freely for the session) | ❌ One decision, many consequential actions | ✅ Before-execution, technically | Fails. Aggregates unrelated authorization decisions into a single evaluation. |
| Tool-registration-time authorization (server startup declares which tools the agent may call; no per-call evaluation) | ❌ | ❌ | Fails both. A tool that is generally allowed may still be denied on a specific invocation because the budget is exhausted, a change-freeze is active, or the target environment is in incident. |
| Post-hoc authorization (execute, then evaluate whether the action should have been permitted) | ✅ | ❌ | Fails. Consequential actions can't always be rolled back. Post-hoc evaluation is monitoring, not authorization. |
MCP tool-call boundary (each tools/call request is intercepted; the RDP evaluates before the tool implementation runs) |
✅ | ✅ | Passes both. |
Each tools/call MCP request is exactly one intended consequential action. The MCP server has the request fully formed — tool name, arguments, invocation context — before invoking the underlying handler. That's the narrow window the invariants specify, and it's a natural interception point rather than a bolted-on hook.
The MCP tool-call boundary is the composition point that satisfies both invariants. Everything downstream — the architecture, the middleware shape, the receipt format — follows from that choice.
The reference middleware
@aiyoplane/mcp-authz is what we built. Apache 2.0, one small npm package, deliberately not an MCP authorization standard. The composition it demonstrates is drawn like this:
AGENT
│
│ MCP tool call
▼
MCP SERVER
│
│ (wrapped by @aiyoplane/mcp-authz)
▼
┌──────────────────────────────────────┐
│ authz.wrap(toolName, handler) │
│ │
│ 1. build intent from toolConfig │
│ 2. rdp.evaluate(intent) ──────────┼────► AIYO RUNTIME DECISION POINT
│ 3. interpret verdict │ (evaluates policy, budget,
│ • DENY → throw AiyoDenied │ evidence signals, etc.)
│ • ALLOW → verify receipt │ ◄──── signed execution receipt
│ 4. invoke handler(args, ctx) │
│ 5. rdp.close(intentId, outcome) ──┼────► AIYO (closes lineage)
└──────────────────────────────────────┘
│
│ result flows back to agent via MCP
▼
AGENT
Integrator code, in full:
import { createAiyoMcpAuthz, createLocalRdp } from '@aiyoplane/mcp-authz';
import policy from './policy.example.json' with { type: 'json' };
const authz = createAiyoMcpAuthz({
rdp: createLocalRdp({ policy }),
toolConfig: {
deploy_to_production: {
aiyo_gated: true,
action: { type: 'deploy', target: 'production' },
},
search_docs: { aiyo_gated: false },
},
});
const deploy = authz.wrap('deploy_to_production', async (args, ctx) => {
// Runs only if the RDP returned ALLOW. ctx.receipt is the verified receipt.
return await actuallyDeploy(args);
});
Register deploy with your MCP server the usual way. The MCP protocol between agent and server is unchanged; the agent never learns that Aiyo is in the loop. The composition is entirely a server-side implementation choice by the merchant running the MCP server.
What it does:
- Intercepts every gated MCP tool call at the exact composition point the invariants specify.
- Delegates the authorization decision to an RDP provider — either
createLocalRdp(in-process, zero credentials, evaluates a small policy) orcreateHostedRdp(composes above Aiyo's production RDP over HTTPS). - Verifies the returned execution receipt locally against the RDP's published verification key. No round-trip needed at the moment of execution.
- On ALLOW, invokes the underlying handler with the verified receipt on the call context.
- On DENY, throws a typed
AiyoDeniederror the MCP server catches and surfaces to the agent as a structured MCP error. - On any tamper, mismatch, expiry, or transport failure, fails closed. The tool never runs.
- After the handler completes (or throws), closes the execution lineage as a fire-and-forget call. A close failure never itself surfaces to the agent.
What it isn't:
- Not an MCP server. Bring your own — the reference SDK, a community framework, or your own implementation.
- Not a replacement for Aiyo's HTTP API. The hosted-RDP provider calls it.
- Not an MCP authorization standard. It's a composition example.
- Not stateful. Every gated tool call is a fresh authorization decision. There's no session cache to invalidate, no policy snapshot to drift.
- Not economic-logic. Pricing, billing, and settlement live server-side on Aiyo.
The full architecture doc lives in the repo: docs/ARCHITECTURE.md.
Engineering decisions we made, and why
We made the RDP pluggable, with a local implementation as the default demo path. The package ships two RDP providers. createLocalRdp is a real, in-process RDP: it evaluates a small policy against the intent and issues receipts using a locally-generated signing key. createHostedRdp is the same interface backed by Aiyo's production API over HTTPS. The wrap-and-execute flow is identical in both modes — the composition pattern is real whether the RDP is local or hosted. A stranger who clones the repo can run the demo in 60 seconds with no credentials, no environment variables, no Aiyo account. That was load-bearing: a public reference that requires signup before doing anything interesting is a business-development lead form dressed up as a package. This is neither.
We built with a fail-closed default at every failure mode. Intent-creation failure, receipt verification mismatch, receipt expired, RDP unreachable, unexpected response shape — all throw a typed error and the tool never runs. Fail-open behavior for specific classes of failure (treat AiyoUnreachable as allow-and-log during Aiyo API outages, for example) is a deliberate operator choice at the integration site, not a middleware default. Fail-open at the middleware level would inject silent risk into every merchant's authorization posture. Fail-open at the integration site is a written decision with a code review trail.
We used Ed25519 with a published JWKS for the production receipt-verification model. New receipts ship in a v2. compact format: v2.<base64url(payload)>.<base64url(ed25519_sig)>, with claims { iid, cid, mid, exp, iat }. Aiyo holds the private key; the public key is served as a standard JWKS at api.aiyoplane.com/.well-known/aiyo-jwks.json. Merchants never hold Aiyo signing material — the middleware fetches the JWKS on first use, caches the keys, and refreshes on demand if verification fails against the current cache (key rotation is handled gracefully). The version prefix (v2.) hardcodes the signing algorithm; no negotiation, no downgrade path, no alg=none vulnerability by construction. Future signing algorithms would take a new prefix (v3.…) rather than extending v2.
The local RDP still uses a v1. HMAC-SHA256 format, because signer and verifier live in the same process — there's no cross-party trust boundary to worry about, and the smaller HMAC dependency keeps the demo code minimal. The verifier dispatches on the version prefix, so v1 and v2 tokens both flow through the same verifyReceipt API without the caller needing to know which they're holding.
We built the payment and non-payment cases as one shape, differing by one field. A payment tool sets action.type: 'payment', passes a dynamic amount, and the RDP additionally consumes an on-rail settlement evidence signal before issuing the receipt. A non-payment tool sets action.type to whatever the consequential action is (deploy, crm_write, paid_api_call, data_export), leaves amount at its default of 0, and the RDP evaluates the merchant-declared policy without any settlement wait. Same primitive, same wrap flow, same receipt, same lineage. The reference demo runs both in the same script — a deploy tool that authorizes to staging and denies to production, and a payment tool that allows small amounts and denies large ones. Both flow through the RDP. Both produce receipts. That is what makes the architectural claim structural rather than payment-adjacent.
We wrote a small, transparent policy evaluator for the demo, and were explicit about what it is and isn't. The local RDP evaluates a JSON policy with rules that match action type, target, and amount thresholds — no evidence-signal composition, no workflow approvals, no cross-actor rules. It's a policy engine only in the way a paper airplane is an aircraft. That's deliberate: the reference is meant to show the composition pattern clearly, not to become a general policy engine. Production Aiyo policies are richer; the local RDP is a legible substitute for the demo path.
What this demonstrates about the broader architecture
Aiyo is the Economic Execution Authorization plane for autonomous systems. Verify First. Execute Second. The primitive is the Runtime Decision Point — a policy-aware gate that evaluates evidence signals against merchant-owned policy and issues a portable, cryptographically-signed execution receipt when the conditions hold.
One of the four architectural stances we describe in the Reference Architecture is that the RDP is action-agnostic — the consequential action being authorized doesn't have to move money. Payment settlement is one possible evidence signal the RDP can consume, and the strongest one where money is the consequence. Equally valid signals include an approved PO, a human approval, a budget check against an ERP, a code-review sign-off, an SSO group membership, or a webhook from an upstream system. The primitive is authorization at the moment before execution — not payment authorization narrowed to one shape.
That's a claim we have made in writing. This ship makes it structural.
The reference package's default demo path is not a payment. The action is a deploy. The RDP evaluates policy, returns an ALLOW verdict, issues a receipt, the middleware verifies the receipt locally, the tool executes. Then a payment tool runs through the same primitive, the same wrap, the same receipt shape — allowed under one amount, denied above the threshold. Same primitive, different action types.
You can read the code. You can run it in a minute. The action-agnostic claim is no longer a paragraph of prose; it's software behavior you can inspect and modify. This is what we mean by "structural."
And it's the second iteration of a pattern we've been running. One week ago — earlier this September 2026 — we shipped the Aiyo × Tempo/MPP adapter, an execution-authorization layer composing across a materially different machine-payment protocol, with the mppx issue #395 validBefore timing bug (credit: beckitrue discovered it upstream) mitigated at the adapter boundary. That ship put the rail-agnostic stance into inspectable engineering practice against a materially different machine-payment protocol. This ship, this week, does the same for the action-agnostic stance in a public reference. Each iteration takes one architectural claim from the Reference Architecture and lands it as inspectable engineering evidence: a shipped surface, a documented pattern, and a real thing developers can point at. The iterations compound.
Kick the tires
- Reference implementation:
github.com/aiyoplane/mcp-authz— Apache 2.0.git clone && npm install && npm run demo. - Architecture doc:
docs/ARCHITECTURE.md— the composition pattern in one page. - Live demo:
mcp-authz.aiyoplane.com— an MCP-mediated agent invoking a non-payment consequential action, gated by the RDP in real time. - Inspect the receipts this reference issues with
@aiyoplane/verify— the standalone offline Ed25519 verifier for Aiyo execution receipts. Any receipt the local RDP or the hosted RDP issues can be independently verified vianpx @aiyoplane/verify path/to/receipt.txt, no coordination with Aiyo required at verification time. The full list of shipped Aiyoplane packages and rail adapters lives on the Trust surface. - Governance pattern this ship surfaced (candidate, from Aiyo's internal Governance Pattern Library): MCP Tool-Call Boundary as RDP Composition Point — the two invariants any RDP composition above a tool-invocation protocol must satisfy, stated generically so the same pattern applies to future integrations against other tool-invocation surfaces. Available on request during design-partner or investor conversations.
If you're building agent infrastructure whose agents perform consequential actions, and the "permission to invoke a tool ≠ authorization for the consequential action" gap is one you recognize, we'd like to talk. Early Access at aiyoplane.com.
If you find bugs, please open an issue. If you find engineering decisions you'd have made differently, we want to know — the point of shipping the reference is to be judged by it.
Verify First. Execute Second.
Building agent infrastructure whose agents perform consequential actions?
Click Early Access at the top of any page and drop your email. Rashon reads every request personally and responds within one business day.
Get early access →