agent-authz-broker
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@agent-authz-brokerread account 8842 and issue 50 credits"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
agent-authz-broker
An agent can request an action. It cannot manufacture the authority to perform one.
A validly signed agent token is not, by itself, authorisation. It might have been minted for a different service. It might claim a scope the human who delegated it never had. It might be asking for something irreversible that nobody approved. A resource server that checks the signature and stops has caught none of those three.
This is an MCP resource server that checks all three, and a suite that proves each check is load-bearing by removing it and watching the tests go red.
Live console: https://agent-authz-broker.vercel.app
Live MCP/API: not deployed. Render and Neon need an owner login this build could not perform —
see Deployment status. The container is built and exercised on every CI run,
and artifacts/deployed-smoke.json is that run's evidence.
The measured result
Six scenarios, two verifiers, one database. Written by
python -m agent_authz_broker.demo into artifacts/matrix.json; every
effect count is SELECT count(*) FROM irreversible_effect from a clean database. No number below
was typed — and CI re-measures on every push and fails if one of them stops reproducing, because
"nobody typed it" is a claim about a file, and a file cannot vouch for itself.
Scenario | Naive baseline | Hardened | Effects (naive → hardened) |
Correct audience, attenuated scope, matching approval | allowed | allowed | 1 → 1 (required) |
Valid token, minted for another resource server | ⚠ allowed | denied | 1 → 0 |
Delegated token claims a scope its delegator lacked | ⚠ allowed | denied | 1 → 0 |
Irreversible tool, no recorded approval | denied | denied | 0 → 0 |
Expired approval | denied | denied | 0 → 0 |
One approval, two calls | denied on the 2nd | denied on the 2nd | 1 → 1 (required) |
The naive baseline permits 2 of the 5 attacks. The hardened server permits 0. Total irreversible effects: naive 4, hardened 2 — and both of the hardened server's two are required, one for the valid request and one for the legitimate first call of the replay pair.
Be precise about what the baseline is. It is the check ADR-001 predeclared before any code
existed: verify the signature against the JWKS, check exp, read the leaf's scope claim. Both
verifiers share the same approval layer, so they can only differ where the difference is a token
check. That is why the last three rows are identical, and ADR-001 predicted otherwise — it said
the naive verifier would also fail the no-approval scenario. It does not. The measurement corrected
the prediction and ADR-001 keeps both.
This repository does not claim MCP servers in general are insecure, and does not claim any product ships the naive check.
Related MCP server: AgentGuard MCP Server
The three checks
1. Audience — the bug class a signature check cannot see
token: iss ✓ sig ✓ exp ✓ scope ✓ aud = https://other-service.example/mcp
we are https://broker.example/mcp
→ denied, audience_mismatch, 0 effectsA token the agent legitimately holds for another service is refused before any tool runs — the
verifier returns None, so the transport rejects it. The SDK's own validate_token_resource stays
on behind that as a second line, not the first.
2. Delegation attenuation — the intersection, never the leaf's claim
alice account:read account:flag
agent-7 account:read account:flag credit:issue ← what the leaf claims
─────────────────────────────────────────────────────
effective account:read account:flag ← what the server computes
→ denied, insufficient_effective_scope, 0 effectseffective = leaf ∩ act[0] ∩ … ∩ root. Monotone, with no branch anywhere that adds a scope back.
A multi-hop test pins the case that matters: a scope missing from the middle link is missing
from the result, which an implementation checking only root and leaf would let through.
3. Human approval, spent exactly once
The irreversible tool's input schema is exactly {account, amount}. There is no field a caller
can use to assert approval — no approval_id, no approved, no justification-as-authority — and
a test walks the advertised schema to keep it that way. The server finds the approval itself, bound
to subject, tool, account, amount, expiry and consumed-state.
One approval authorises one effect because of two things that are not Python:
UPDATE approval SET consumed_at = now()
WHERE approval_id = :id AND consumed_at IS NULL -- only one statement can match…behind a UNIQUE constraint on irreversible_effect.approval_id. An asyncio.Lock would pass the
concurrency test and fail behind two workers, so there is no lock in this repository.
The adversarial review: eleven breaches planted, eleven caught
A suite that has never failed is not evidence the system is safe. Each control was removed in turn and the suite re-run.
Breach planted | Result |
| scenario A failed |
attenuation returns the leaf's claim | both scenario B tests failed |
| scenario D failed — as an |
approval lookup stops filtering on the account | scenario C |
an | two surface tests failed |
the approver credential no longer gates | two boundary tests failed |
a refusal at the transport is not written to the audit trail | two boundary tests failed |
| the parse test failed, as a raw crash |
| the session refused to start |
one scenario stops reading its effect count from the database | the predeclaration guard failed |
the rate-limit claim becomes read-then-write | two rate-limit tests failed |
Breach 3 is the informative one: removing the application-level guard did not produce a wrong answer, it produced a constraint violation. The database refused the second effect on its own, which is the only reason it is honest to call that constraint a backstop rather than decoration.
The last three come from a second review, which attacked a copy rather than reading it and found
two material holes — neither in a control the first review had tested. POST /api/v1/approvals was
unauthenticated, so an agent could create the approval it then spent; and a token refused at the
transport produced no audit row at all, so the trail was blind to precisely the attacks above.
ADR-004 has both, including why no test in the suite was positioned to catch the
second.
The last two come from a third review, which asked what did nobody look at? — and found that
the guard protecting the kill test was itself the mistake it warns about. All three of its checks
read the kill test as text, so adding pytest.mark.skip left them reporting 3 passed while
zero of the seventeen kill tests ran. It now asks pytest what it is about to run instead of reading
source. The same review found the container entrypoint calling a module that has never existed —
ADR-005.
Re-run it yourself — the table is a script, and CI runs it. make breaches replants all eleven
against your checkout, requires the named tests to fail each time, restores every file, and exits
non-zero if any control turns out not to be load-bearing. All controls restored; 51 tests
green.
Real MCP, over Streamable HTTP
Driven by the SDK's own client against a running server, not asserted:
server: agent-authz-broker 0.1.0
protocol: 2025-11-25
tools: ['flag_account', 'issue_credit', 'ping', 'read_account', 'read_approval', 'request_approval']/.well-known/oauth-protected-resource/mcp serves RFC 9728 protected-resource metadata. Reproduce
with uv run python scripts/mcp_smoke.py.
What MCP conformance does and does not cover. Protocol conformance says this is a well-formed MCP server — initialize, tool listing, schemas, transport. It says nothing about audience validation, delegation attenuation or approval semantics. Those are this repository's own adversarial suite, and the two are never presented as one result.
What you are looking at
The security matrix — every scenario, both verifiers, effects counted from the table.

The delegation chain — where authority was lost, rather than an assertion that it was.

Approvals — target, expiry, and the consumed state that makes replay impossible.

The audit trail — every decision, refusals included, with what it was decided from. Refusals made at the transport, before any tool is routed, are written by the verifier itself: they used to be silent, which meant the trail was blind to precisely the attacks above.

Honest limits
The approval-granting endpoint authenticates a credential, not a person.
POST /api/v1/approvalsis gated onAAB_APPROVER_TOKEN— unset and the route is disabled, and no bearer token the agent can hold will satisfy it. That is the boundary the claim needs and it is the whole of it: one shared secret stands in for whatever authenticates staff, so anyone holding it can approve as anyone, andapproved_byis a string that holder supplied.This endpoint was open in every environment until a security review broke the headline claim with it. The reasoning had been that the MCP surface cannot reach it — an agent may
request_approvaland cannot create one. But the agent is a process, not a tool list: mint a token,POSTan approval naming its own subject, callissue_creditover real MCP, andSELECT count(*) FROM irreversible_effectreturns 1. "No MCP route to it" is not the boundary that matters when both surfaces share one origin.The demo token mint is open on any non-production instance.
GET /api/v1/demo/tokenshands five signed tokens — valid, wrong-audience, scope-amplifying, expired, forged — to any anonymous caller, because a visitor who cannot get a token cannot drive the lab. It is safe to say out loud: a token is the input this server exists to disbelieve, and since approving now needsAAB_APPROVER_TOKEN, which this endpoint does not mint, an anonymous visitor holding every one of those tokens still cannot cause an irreversible effect. SetAAB_ENVIRONMENT=productionand the endpoint is gone.read_approvalleaves no audit row. It is the one tool that reaches no verdict and can cause no effect, so it does not go through the decision path — which means an agent can probe it for approval ids untraced. It answersnot_founduniformly so the probe learns nothing, and rate limiting, which is what would actually bound it, is not built.This is a resource server, not an authorization server. The test authority mints tokens with real Ed25519 keys and is otherwise not an IdP: no clients, no consent, no discovery, no refresh. It is a lab instrument.
A stolen, still-valid token is not detected. The design limits the blast radius to the attenuated scope and stops irreversible action without approval. It does not stop a thief using a live token within its scope for reversible reads. See
docs/threat-model.md.Everything is synthetic. Invented accounts, invented balances, no payment rail, no external call. The "irreversible effect" is a row in a demonstration table.
The rate limit is a ceiling, not a defence.
(verified subject, tool, fixed 60-second window), default 30, counted in PostgreSQL so the bound does not depend on how many workers are running. Fixed windows, so the worst case across an arbitrary minute is twice the limit. It is not DDoS protection and is not part of the security claim: remove it and the kill test admits not one extra attack. ADR-006 says why it is here at all.Not built: the blueprint's Keycloak gap analysis, OpenTelemetry/Langfuse, step-up authorization, CIMD-vs-DCR, and the full RFC 9728/8707/9207 conformance suites. ADR-002 lists each one.
Deployment status
Layer | State |
Console — Vercel Hobby | Live, https://agent-authz-broker.vercel.app, rendering the committed artifacts |
MCP resource server — Render Free | Not deployed |
PostgreSQL — Neon Free | Not provisioned |
Both need an account login, so the hosting step is the owner's. Everything that does not need the
account is done and checked: the image builds, the entrypoint migrates to head and serves, and
scripts/deployed_smoke.py drives every boundary below against that running container over HTTP —
counting effects out of irreversible_effect, not from what a tool call says about itself. CI builds
the image and runs exactly that, so the deployment is exercised on every push rather than described.
Point the same script at a Render URL when one exists and it produces the same artifact:
AAB_APPROVER_TOKEN=... uv run python scripts/deployed_smoke.py --base-url https://your-hostRun it
make install # uv sync --frozen
make db-up # PostgreSQL
make migrate # alembic upgrade head
make killtest # the adversarial suite against real PostgreSQL
make breaches # replant all ten breaches; each must turn its own tests red
make matrix-gate # prove every number above still reproduces from a fresh run
make api # the MCP server + console API on :8000cd frontend && npm install && npm run dev # the console on :3000Documents
File | Purpose |
ADR-001: the claim, threat model and kill test, declared before implementation. ADR-002: what is not built. ADR-003/004/005: three adversarial reviews, each finding what the previous ones could not — the third found that the container could not have booted | |
each adversary capability, its mitigation, and what is not mitigated | |
topology, what is deployed and what is not, and §11's split between what was run and what was merely written | |
the operating rules this repository is built under |
This server cannot be deployed
Maintenance
Related MCP Connectors
Runtime permission, approval, and audit layer for AI agent tool execution.
Supervised API-write gateway for AI agents with policy, human approval and execution receipts.
Pre-execution policy gate for consequential agent actions with durable trust receipts.
Human approval for irreversible AI agent actions, bound to the exact tool call by a passkey tap
Related MCP Servers
- AlicenseNot gradedqualityBmaintenanceGates agent tool execution with human approval, audit trails, and replay-resistant permits, enabling safe use of tools in agent loops.15 PyPIMIT
- FlicenseNot gradedqualityBmaintenanceProvides a secure MCP boundary for AI agents, intercepting and validating tool calls, redacting secrets, and requiring human approval for sensitive actions with a tamper-evident audit trail.-
- AlicenseNot gradedqualityBmaintenanceProvides a human-in-the-loop approval gateway for AI agents, enforcing policies and audit logging for MCP-compatible tool calls.Apache 2.0
- FlicenseNot gradedqualityCmaintenanceEnables controlled AI-agent access to enterprise-shaped tools with a deny-by-default gated write path, human approval, dry-run execution, and append-only audit logging.1-