---
name: grantor-mcp
description: "Use when a parent agent needs to hand a sub-agent scoped, revocable authority instead of its own full credentials — grant a bounded capability, check it before every tool call, narrow it further for a handoff, or revoke it"
---

# grantor-mcp

Spinning up a sub-agent (a worker, a delegate, a tool-calling loop you don't
fully trust) and handing it your own credentials means it can do anything
you can, forever, until you rotate your key. `grantor-mcp` is a permission
broker: it holds a bounded, on-chain-anchored capability for each sub-agent
— named tools only, a use budget, an expiry — and you check every action
against it before letting the sub-agent proceed. This kit is that broker,
exposed as five MCP tools (`grant`/`check`/`delegate`/`revoke`/`status`).

## When to use this

**A sub-agent needs scoped authority, not your credentials.** If you're
about to pass an API key, a signing key, or a "do anything I can do" prompt
down to a worker you spawned, stop — grant it a bounded capability instead.
Also reach for this when a capability you already hold needs to be handed
onward, narrower than you have it (delegation), or pulled back before its
natural expiry (revocation).

Not what this is for: authenticating a human, or verifying a deed presented
by an arbitrary caller you don't control the identity of. That's
[`@grantor/verify`](https://www.npmjs.com/package/@grantor/verify)'s
`DeedGuard` — a different, complementary piece.

## The three moves

### 1. Grant, then check before every action

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

Hand the sub-agent `child_id` (never a key — the broker holds it). Before
the sub-agent calls any tool, check it:

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

Loop this on every single sub-agent action, not just the first — the check
is cheap, and it's what makes the budget and expiry real rather than
advisory. A denied check comes back as `{allow: false, code, reason}`, never
a thrown error — branch on `code` (table below).

### 2. Narrow for a handoff

If the sub-agent itself needs to spin up a further sub-agent, don't grant a
second capability from scratch — narrow the one it already has. This proves
the second sub-agent can never end up with MORE authority than the first,
because narrowing is enforced both when the link is signed and again,
independently, when it's verified:

```
delegate({parent: "a1b2...", tools: ["search"], max_uses: 10})
  -> {"child_id": "c3d4...", "sub": "7e21...", "grants": [...]}
```

Omitting `tools`/`max_uses`/`ttl_secs` inherits the parent's own values
(still bounded to be no wider). Widening is bounded two DIFFERENT ways,
not one: asking for a **tool** the parent doesn't hold is refused before
any key is touched (`delegate` throws, naming the tool). Asking for MORE
`max_uses` or a longer `ttl_secs` than the parent holds is not refused at
all — it's **silently clamped down** to the parent's own bound instead, so
don't assume a successful `delegate` call granted exactly the numbers you
asked for; read the returned `grants` back. Either way, the chain walk
re-checks narrowing cryptographically at verify, independent of what
signing did.

### 3. Revoke

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

On your own tenant (with `GRANTOR_MCP_ADMIN_KEY` set), this is a real
on-chain `bumpEpoch` — it invalidates every outstanding link signed against
that revocation cohort, not just the one child, immediately. In the shared
sandbox it revokes locally at the broker instead (the broker holds the
sandbox key and is the check-point for it) — `revoke`'s own response says
which happened.

## DENY codes — what to do on each

`check` never throws; every refusal is `{allow: false, code, reason}`.

| Code | What happened | What to do |
|---|---|---|
| `CapabilityDenied` | The capability is real but doesn't cover this tool/action. | Don't retry as-is — if the sub-agent legitimately needs this tool, `grant`/`delegate` a wider (but still bounded) capability from a principal that has it. |
| `UsesExhausted` | The local use budget is spent. | Request a fresh `grant` (or `delegate`) from whoever holds the authority — don't just keep calling `check`, it will keep denying. |
| `EpochRevoked` | The revocation cohort this link was signed against was bumped — authority was pulled, on-chain. | **Stop.** This is a deliberate revocation, not a transient error. Do not retry, do not request a new grant from the same source without checking why it was revoked. |
| `BadDelegation` | The delegation chain itself doesn't check out (a widened link, a bad signature, an unenrolled root). | Stop and surface this — it usually means a bug in how the chain was built, not something a retry fixes. |
| `TenantInactive` | The tenant behind this capability isn't billed/active on-chain. | Not your problem to fix — surface it to the operator; nothing authenticates against this tenant until it's funded again. |
| `RevokedLocally` | This specific child was revoked at the broker. | Same as `EpochRevoked` — stop, don't retry. |
| `UnknownChild` | The `child_id` doesn't exist at this broker. | Check you're using the ID from the matching `grant`/`delegate` call, and that you're pointed at the same broker state (same `GRANTOR_MCP_STATE`/config) that issued it. |
| `BadRequest` | Malformed call — neither `child_id` nor both `deed` and `challenge` were supplied. | Fix the call: supply `child_id`, or both `deed` and `challenge` together. This is a caller bug, not a denial of authority. |
| `Chain` | An on-chain read failed (RPC unreachable, etc.). | Transient — a short retry is reasonable; if it persists, surface it, don't fail open. |

## Also usable from the CLI

```sh
node src/cli.js status
node src/cli.js grant --tools search,fetch --max-uses 50 --ttl-secs 3600
node src/cli.js check --child <child_id> --tool search
node src/cli.js delegate --parent <child_id> --tools search --max-uses 10
node src/cli.js revoke --child <child_id>
```

## More detail

`README.md` in this directory has the sandbox-honesty block (what the
published sandbox principal can and can't do), own-tenant setup, and the
full DENY-code table cross-referenced against the verifier-wide one.
[`docs/guide/mcp-broker.md`](../../docs/guide/mcp-broker.md) has the full
product walkthrough and the sandbox-vs-own-tenant comparison.
