# Permission broker (grantor-mcp)

Every multi-agent framework eventually needs to answer: what does the
sub-agent get to do? `grantor-mcp` (`@grantor/mcp`) is a small MCP server
that answers it with a **bounded, on-chain-anchored capability** instead of
a copy of the parent's own credentials. If you haven't already, read
[Concepts](concepts.md) for the deed mental model and
[Capabilities (delegation)](capabilities.md) for the grant grammar this
kit is built on — this page is the packaged, MCP-native version of that
same mechanism.

## The problem

The easy way to give a sub-agent tools is to hand it what the parent
already has: the same API key, the same signing key, the same "call
anything" system prompt. That sub-agent can now do everything the parent
can, for as long as that credential lives, and the only way to stop it is
to rotate the parent's own key — which stops every OTHER sub-agent too.
There is no per-child expiry, no per-child use budget, no way to narrow
what one specific worker is allowed to do, and no cheap way to revoke just
that one worker without a blast radius.

## The model

`grantor-mcp` sits between the parent and its sub-agents as a broker. The
parent asks it to `grant` a sub-agent a capability — named tools, a use
budget, an expiry — and the broker mints and holds a child identity for it,
signed by the parent's own registered root key. From then on the parent (or
the sub-agent itself, if it's MCP-aware) calls `check` before every tool
use; the broker verifies the capability for real — not against its own
bookkeeping alone, but against the public chain:

- **A registered root key.** Every capability chain roots at a key
  registered on `GrantorRegistry` for a specific tenant. An unregistered or
  revoked root fails verification outright.
- **A live revocation epoch.** Every link is signed against a revocation
  cohort at a specific epoch. The tenant admin can bump that epoch on-chain
  — one transaction invalidates every outstanding link signed against the
  old value, immediately, no per-link bookkeeping.
- **Tenant billing.** The tenant behind the capability must be `Active` or
  `Grace` on-chain, or nothing verifies — the same billing gate every other
  deed mode enforces.

A capability can only ever get **narrower** as it's handed onward — fewer
tools, a shorter expiry, a smaller use budget. The broker enforces this at
signing time, but not identically for every field: asking `delegate` for a
**tool** the parent doesn't hold is **refused outright**, before any key is
touched. Asking for more **uses** or a **longer expiry** than the parent
holds is not refused at all — it's **silently clamped down** to the
parent's own bound instead, so a `delegate` call can succeed while granting
fewer uses or less time than requested. Either way, narrowing is enforced a
second time, independently, at verification: a forged or bypassed-broker
deed asserting more than its chain actually delegated is refused by the
cryptographic chain walk itself, not by anything the broker remembered.

## Walkthrough

The transcript below is the shape of `sdk/mcp/e2e.sh`'s live run (grant →
delegate → check ALLOW → escalation DENIED → revoke), shown as MCP tool
calls. The same five calls work identically from the CLI
(`node src/cli.js <verb> ...`) or over MCP stdio from any client.

**1. Grant a sub-agent a bounded capability** — search and fetch, 5 uses,
one hour:

```
grant({tools: ["search", "fetch"], max_uses: 5, ttl_secs: 3600})
  -> {"child_id": "a1b2c3d4-...", "sub": "9f3c...", "grants": [...], "exp": 1786020000}
```

The broker signs a `Delegation` under the hood, rooted at the operator's own
registered agent key, and holds the resulting child identity — `child_id`
is what the parent hands the sub-agent, never a key.

**2. Delegate a narrower slice onward** — the sub-agent itself spawns a
worker, and gets `search` only, 3 uses:

```
delegate({parent: "a1b2c3d4-...", tools: ["search"], max_uses: 3})
  -> {"child_id": "e5f6a7b8-...", "sub": "7e21...", "grants": [...]}
```

Every field named here is bounded to be no wider than the parent's own
grant, but not the same way: passing `tools: ["search", "write"]` at this
step would be **refused** before any key is touched, naming exactly which
requested grant doesn't narrow any parent grant — `write` isn't in the
parent's tool set, and there's no bound to clamp it to. Passing
`max_uses: 999` instead would NOT be refused; it would silently come back
clamped to whatever the parent's own remaining bound actually is.

**3. Check — ALLOW:**

```
check({child_id: "e5f6a7b8-...", tool: "search"})
  -> {"allow": true, "remaining_uses": 2}
```

A real on-chain verification ran here: the chain of custody back to the
registered root key, the live revocation epoch, and the tenant's billing
status, plus the offline narrowing check and the local use-budget
decrement. `remaining_uses` just dropped from 3 to 2.

**4. Check — escalation DENIED:**

```
check({child_id: "e5f6a7b8-...", tool: "write"})
  -> {"allow": false, "code": "CapabilityDenied",
      "reason": "grant does not cover mcp://grantor-mcp/tools/write"}
```

The worker's capability is genuine — the check above just succeeded — it
simply never covered `write`. This is the deliberately distinct case from
every other refusal: the deed checked out, the caller is authenticated, it
just isn't authorized for this specific action.

**5. Revoke:**

```
revoke({child_id: "a1b2c3d4-...", epoch_label: "..."})
  -> {"revoked": "onchain", "tx": "0x...", "epoch_label": "..."}
```

On your own tenant this is a real `GrantorRegistry.bumpEpoch` — and because
the worker's delegation (step 2) was signed against the SAME revocation
cohort as its parent, this one transaction invalidates both the parent's
capability and everything delegated from it, immediately:

```
check({child_id: "e5f6a7b8-...", tool: "search"})
  -> {"allow": false, "code": "EpochRevoked",
      "reason": "link signed at epoch 0, tenant is now at epoch 1"}
```

No per-child cleanup, no separate revocation call for the worker — one
`bumpEpoch` on the shared cohort took out the whole sub-tree it rooted.

## Sandbox vs. your own tenant

The package ships with a working sandbox out of the box (`npx @grantor/mcp
serve`, no config) so you can run the walkthrough above immediately. It
trades real limits and revocation for zero setup:

| | Shared sandbox (default) | Your own tenant |
|---|---|---|
| Setup | None — bundled config | `GRANTOR_MCP_CONFIG` pointing at your registered tenant |
| Principal key | Published, shared by everyone who runs the package | Yours; only you hold it |
| Controls funds / admin power | No — registered for delegation only, no billing power | Whatever your tenant's admin key controls |
| Revocation (`revoke`) | Local, at the broker (the sandbox key's only check-point) | Real on-chain `bumpEpoch` — invalidates every outstanding link, everywhere, instantly |
| Billing / limits | Shared, operator-funded | Your own — your tenant, your tier, your caps |
| Use-budget metering | Broker-local (this process's state file) | Same mechanism, but now it's YOUR broker instance and YOUR state |
| Good for | Trying the arc, local development, demos | Anything a sub-agent's authority should actually matter for |

Full field-by-field config shape, env vars, and the operator side of
standing up a shared broker deployment: [docs/deploy/mcp-sandbox.md](../deploy/mcp-sandbox.md).
The `sdk/mcp/README.md` sandbox-honesty block spells out exactly what the
published sandbox key can and can't do, in more detail than the table
above.

## Getting your own tenant

Becoming a tenant is a handful of on-chain transactions against
`GrantorRegistry` — no signup form, no account. The
[register page](../../register.html) walks any wallet through it (USDC on
Base, funds land in your own tenant balance, nothing else touches a
Grantor server because there is no Grantor server). Point `grantor-mcp` at
the result with `GRANTOR_MCP_CONFIG` and you have real limits, real
revocation, and a billing relationship that's entirely yours.

## See also

- `sdk/mcp/README.md` — the package README: the 60-second first run, the
  full tool reference, and the DENY-code table.
- `sdk/mcp/SKILL.md` — the agent-facing quick reference: tool-call
  sequences and what to do on each DENY code.
- [Capabilities (delegation)](capabilities.md) — the underlying grant
  grammar, narrowing rules, and revocation mechanics this kit packages.
- [Errors](errors.md) — the full verifier-wide error/code table.
