Reference middleware for composing Aiyo — the settlement-verified Economic Execution Authorization plane for autonomous systems — above an MCP server. Verify First. Execute Second. Learn more at aiyoplane.com.
@aiyoplane/mcp-authz is a small, Apache 2.0-licensed reference implementation of one pattern: composing an independent authorization decision between an autonomous agent's tool-call intent and the tool's actual execution, at the MCP protocol boundary.
MCP makes it easy for an autonomous agent to invoke a tool. Permission to invoke a tool, however, is not necessarily the same thing as authorization for the consequential action that invocation will cause. A production deploy, a database write, a paid third-party API invocation, a CRM change on behalf of a customer — each is a consequential action whose runtime conditions (policy, budget, approval, freeze window, on-call state, capability lifetime, cumulative rate) may be independent of whether the agent is generally allowed to call the tool.
Aiyo's Runtime Decision Point (RDP) sits at exactly that boundary. This package is a thin adapter: wrap your MCP tool handler with authz.wrap(...), and every invocation flows through an RDP before the tool runs.
The reference is deliberately small. It demonstrates the composition pattern rather than attempting to become an MCP authorization standard or replace MCP itself.
MCP tool invocations are the correct semantic granularity for an execution-authorization decision — one authorization per tool call, at the boundary where consequential action begins. Payment settlement is one possible evidence signal an RDP can consume; the primitive works when the consequential action doesn't move money at all. This package's default demo exercises the non-payment case as the primary example, because that is what makes the architectural claim structural rather than payment-adjacent.
git clone https://github.com/aiyoplane/mcp-authz.git
cd mcp-authz
npm install
npm run demoYou'll see five labeled sections showing an ungated bypass, a non-payment ALLOW (deploy to staging), a non-payment DENY (deploy to production), a payment ALLOW ($10), and a payment DENY ($100). The same Runtime Decision Point governs all four gated calls.
No credentials. No environment variables. No network round-trip. The demo runs against a local, in-process RDP that evaluates a small policy from policy.example.json at the repo root and signs receipts with a per-process key. The receipt format is byte-for-byte the same as Aiyo's production server-side signer, so the middleware's verifier code path is exercised identically to how it runs in production.
The middleware takes an rdp provider. Two providers ship in this package.
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);
});Zero credentials. The RDP evaluates policy.example.json in-process and issues receipts against a locally-generated signing key. Use this for examples/, tests, and local iteration.
import { createAiyoMcpAuthz, createHostedRdp } from '@aiyoplane/mcp-authz';
const authz = createAiyoMcpAuthz({
rdp: createHostedRdp({
apiBaseUrl: 'https://api.aiyoplane.com',
apiKey: process.env.AIYO_API_KEY,
}),
toolConfig: { /* ... */ },
});No shared secret is required for verification. Aiyo signs receipts with an Ed25519 private key it holds; the public key is published as a standard JWKS at https://api.aiyoplane.com/.well-known/aiyo-jwks.json. The middleware fetches the JWKS the first time it verifies a receipt and caches the keys, refreshing on demand if verification fails against the current cache (key rotation is handled gracefully). Non-custodial by construction — nothing custodial ever leaves Aiyo.
Access to production Aiyo credentials (the apiKey for creating intents) is granted through the Early Access surface at aiyoplane.com.
The wrap-and-execute flow is identical in both modes. The composition pattern is real whether the RDP is local or hosted. That's the point of the reference.
- Intent. The MCP server receives a tool call.
authz.wrap(...)intercepts, extracts the tool's action metadata fromtoolConfig, and builds an intent payload including anyattributioncontext you pass (workflow_id,agent_id). - RDP evaluation. The middleware calls
rdp.evaluate(intentPayload). The local RDP evaluates the policy in-process. The hosted RDPPOSTs the intent to the RDP evaluate endpoint on your configured Aiyo API base URL. - Verdict.
- DENY → the RDP throws
AiyoDeniedwith the rule/field that failed. Your MCP server catches this and returns an error to the agent. - ALLOW → the RDP resolves with a signed execution receipt. The middleware verifies the receipt locally against the RDP's signing key — no round-trip.
- DENY → the RDP throws
- Execute. The middleware invokes your original tool handler, passing the verified receipt on
ctx.receipt, the decoded payload onctx.receiptPayload, and the parent intent's id onctx.intentId. - Close the lineage. Once the handler returns (or throws), the middleware calls
rdp.close(intentId, outcome). Local RDP is a no-op; hosted RDP POSTs the outcome to the RDP so the Aiyo dashboard's lineage panel renders the full chain from agent intent through RDP verdict through tool invocation through outcome. The close call is fire-and-forget — a close failure never itself causes the tool call to fail.
Same shape whether the tool is a payment tool or a non-payment tool. Payment tools set action.type: 'payment' and pass an amount; non-payment tools default to amount: 0.
The examples/mcp-server-basic folder ships four tools: a mock production deploy (denied by policy), a mock staging deploy (allowed), a payment tool (allow/deny by amount), and an ungated read-only search. Neither of the non-payment tools moves money. Both flow through the RDP. Both produce an execution receipt in the lineage.
That is the point of this reference: to show, in inspectable code, that Aiyo's authorization primitive sits above the execution surface — not intrinsically inside a payment flow.
Two compact formats coexist. New receipts are v2. The verifier accepts both.
v2 (current — Ed25519, public-key verification):
v2.<base64url(json_payload)>.<base64url(ed25519_sig)>
Aiyo signs with a private Ed25519 key. Verifiers use the corresponding public key published at api.aiyoplane.com/.well-known/aiyo-jwks.json in standard JWKS format. Merchants never hold Aiyo signing material, and key rotation propagates through the published key set without distributing new secrets.
v1 (legacy — HMAC-SHA-256):
v1.<base64url(json_payload)>.<hex(hmac_sha256_sig)>
Retained for backward compatibility with receipts issued before September 2026. The local RDP demo also uses v1 with a per-process randomly-generated HMAC key — that's safe because signer and verifier are in the same process (no cross-party trust boundary). Hosted-RDP integrators can opt into v1 acceptance by passing hmacKey on createHostedRdp config; this is only needed if you have in-flight v1 receipts to accept across the migration.
Both versions carry the same payload claims: { iid, cid, mid, exp, iat }. Expiry is enforced with a configurable clock-skew leeway (default 30 seconds). The version prefix hardcodes the signing algorithm — no negotiation, no downgrade path, no alg=none vulnerability.
createAiyoMcpAuthz(config) accepts:
| Field | Required | Description |
|---|---|---|
rdp |
yes | RDP provider from createLocalRdp(...) or createHostedRdp(...), or your own object implementing { evaluate, close, verifyReceipt }. |
toolConfig |
yes | Object keyed by tool name. Each entry declares either aiyo_gated: false (skip the RDP) or aiyo_gated: true with an action object naming the consequential action. Optional amount (number or function of args) and currency fields make the tool a payment tool. |
intentBuilder |
no | Function (toolName, args, ctx, toolDef) => intentPayload for full control over intent shape. Defaults to a sensible builder. |
logger |
no | Logger with .info and .error. Defaults to console. |
verifyReceiptOnAllow |
no | Verify the receipt after ALLOW (default: true). |
createLocalRdp(opts) accepts: policy (required), merchantId (default 'demo-merchant'), signingKey (default: random 32 bytes), ttlSeconds (default 300), clockSkewSeconds (default 30), logger.
createHostedRdp(opts) accepts: apiBaseUrl, apiKey (both required); optional jwksUrl (defaults to ${apiBaseUrl}/.well-known/aiyo-jwks.json), hmacKey (accepts legacy v1 receipts), fetch, timeoutMs, clockSkewSeconds.
The authz.wrap(toolName, handler) function returns a wrapped async handler with signature (args, ctx) => Promise<result>. Pass attribution: { workflow_id, agent_id } on ctx for two-actor attribution.
- It does not implement an MCP server. Bring your own — the reference SDK, a community framework, or your own implementation.
- It does not replace Aiyo's HTTP API. The hosted-RDP provider calls it.
- It does not maintain long-lived session state or sticky policy caches. Every gated tool call is a fresh authorization decision.
- It does not implement pricing, billing, or economic settlement logic. Those live on the Aiyo backend.
- It does not attempt to become an authorization standard for MCP. It is a small composition example demonstrating one architectural pattern.
npm install
npm test # runs the unit tests
npm run demo # runs the reference demo (no credentials required)The test suite uses node:test. No Aiyo credentials, no live backend, no network calls required.
See docs/ARCHITECTURE.md for the full composition pattern, including the lineage-close flow and error semantics.
This is a preview release at v0.1. The public API surface (createAiyoMcpAuthz, createLocalRdp, createHostedRdp, wrap, verifyReceipt, error classes) may evolve through the v0.x line. v1.0 locks the API.
Feedback, issues, and PRs welcome.
Apache 2.0. See LICENSE and NOTICE.
Aiyo is the settlement-verified Economic Execution Authorization plane for autonomous systems. Verify First. Execute Second. Its Runtime Decision Point evaluates policy, authority, budget, approval, payment settlement (where relevant), and other runtime conditions at the moment a consequential action is about to execute, and issues a portable, cryptographically-signed execution receipt when — and only when — the conditions hold. Quadruple-agnostic: actor-agnostic, rail-agnostic, wallet-agnostic, action-agnostic. Learn more at aiyoplane.com.