New:Socket for Asana Is Now Available.Learn more
Get Started

@grantor/mcp

Package Overview
Dependencies
Maintainers
1
Versions
9
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@grantor/mcp

Grantor permission broker for multi-agent frameworks — grant, delegate, check, revoke bounded capabilities over MCP. No authorization server anywhere.

npmnpm
Version
0.1.2
Version published
Weekly downloads
1.5K
Maintainers
1
Weekly downloads
 
Created
Source

@grantor/mcp

A permission broker for multi-agent frameworks. When a parent agent spins up a sub-agent, the easy thing to do is hand it the same credentials the parent has — every tool, no expiry, no way to take it back short of rotating the parent's own key. grantor-mcp is the other option: an MCP server that sits between the parent and its sub-agents and hands out bounded capabilities — named tools only, a use budget, an expiry — anchored on-chain (a registered root key, a live revocation epoch, a billed tenant), not just an in-memory promise. Widening is bounded two ways: asking for a tool the parent doesn't hold is refused at signing time; asking for more uses or a longer expiry than the parent holds is silently clamped down to the parent's own bound. Either way, the chain walk re-enforces narrowing cryptographically at verify, independent of what the broker did at signing. No authorization server anywhere — this is a library, not a service.

Ships with a published sandbox principal so you can try the whole arc — grant, delegate, check, escalate, revoke — before you own a tenant. See Sandbox vs. your own tenant for exactly what that key can and can't do.

60-second first run

Add it to Claude Code:

claude mcp add grantor-mcp -- npx -y @grantor/mcp serve

Or wire it into any MCP client's config directly:

{
  "mcpServers": {
    "grantor-mcp": {
      "command": "npx",
      "args": ["-y", "@grantor/mcp", "serve"]
    }
  }
}

No API key, no signup, no config file required — the first run uses the bundled sandbox tenant on Base mainnet. Call status first; it always tells you which mode you're in.

The shared sandbox goes live with the package's first publish — the bundled sandbox-config.json is provisioned (a real tenant created and funded, per docs/deploy/mcp-sandbox.md) before 0.1.0 ships. If you're running from source before that happens, every tool — status included — refuses outright with the same clean message ("sandbox not provisioned yet"), rather than one guarded verb failing loud and another failing on the empty placeholder key with a cryptic error. Not something you'll hit against the published package.

The five tools

Every tool is described in full to the MCP client at connect time (inputSchema + a one-line purpose string); this is the short version.

  • status — broker + tenant snapshot: mode, live on-chain tenant standing, principal registration, child/use counters.
    status() -> {"mode":"sandbox","tenant_status":"Active","principal_registered":true,
                 "children":{"total":0,"revoked":0,"uses_remaining":0},
                 "grantor_note":"Shared sandbox tenant. For your own limits..."}
    
  • grant — give a sub-agent a bounded capability: named tools, a use budget, an expiry. Returns a child_id the broker holds and metering.
    grant({tools:["search"], max_uses:3, ttl_secs:3600})
      -> {"child_id":"a1b2...","sub":"9f3c...","grants":[...],"exp":1786020000}
    
  • check — authorize ONE action for a child: real on-chain verification (root key, revocation epoch, tenant billing) + capability match + local use budget. Gate every sub-agent tool call on this.
    check({child_id:"a1b2...", tool:"search"}) -> {"allow":true,"remaining_uses":2}
    check({child_id:"a1b2...", tool:"write"})  -> {"allow":false,"code":"CapabilityDenied","reason":"..."}
    
  • delegate — narrow an existing child's capability onward to a new child (fewer tools, fewer uses, shorter expiry). A tool outside the parent's grant is refused at signing; a wider max_uses/ttl_secs is silently clamped to the parent's own bound instead of refused. Either way, the chain walk re-checks narrowing cryptographically at verify.
    delegate({parent:"a1b2...", tools:["search"], max_uses:1})
      -> {"child_id":"c3d4...","sub":"7e21...","grants":[...]}
    
  • revoke — revoke a child's authority. With your own tenant + admin key this is a real on-chain bumpEpoch; in the sandbox it revokes at the broker (which holds the key and is the check-point).
    revoke({child_id:"a1b2..."}) -> {"revoked":"onchain","tx":"0x...","epoch_label":"..."}
    

grant/delegate accept an optional to (an external holder's deed sub) and return a signed link instead of a broker-held child — for handing authority to an identity the broker does not hold the key for. See The presented-deed caveat.

The sandbox honesty block

The bundled sandbox-config.json ships a real, published principal private key, registered as an agent key on a real Base mainnet tenant, so grant/delegate/check/revoke all work with zero setup. Before you build anything on it, know exactly what it is:

  • It controls nothing outside the demo tenant. The key is not an admin key on anything, holds no funds, and cannot register or revoke agent keys on-chain — its ONLY on-chain power is the one it's registered for (delegation root for this one tenant).
  • No funds, ever. The sandbox tenant's billing is the operator's; you never pay for a sandbox call, and the sandbox never asks for a key that could spend anything.
  • Anyone can read this key. It's checked into the published package. Never reuse it, never fund it, and never treat a deed minted with it as private — every sandbox principal in the world is the same identity.
  • check's self-issued challenge. In broker-held mode, the broker mints a random challenge, signs a deed against it with the child's key, and immediately verifies that same deed — it is both holder and verifier in one process. That's real cryptographic + on-chain verification (the chain reads, the revocation-epoch check, and the billing gate are all live), just not a caller-issued challenge. See the presented-deed caveat for the mode that is.
  • Metering is broker-local. max_uses/remaining_uses live in this process's state file (~/.grantor-mcp/state.json by default), not on-chain. Two brokers, or two runs against the same config without a shared state file, do not share a budget.

None of this is a limitation you have to accept — it's what changes the moment you run your own tenant.

Own-tenant setup

Point the broker at your own registered tenant instead of the shared sandbox:

{
  "mcpServers": {
    "grantor-mcp": {
      "command": "npx",
      "args": ["-y", "@grantor/mcp", "serve"],
      "env": {
        "GRANTOR_MCP_CONFIG": "/absolute/path/to/your-broker-config.json",
        "GRANTOR_MCP_PRINCIPAL_KEY": "0x<your registered agent key>",
        "GRANTOR_MCP_ADMIN_KEY": "0x<your tenant admin key, for revoke>"
      }
    }
  }
}

Env vars, all optional except when the mode they gate needs them:

VarMeaning
GRANTOR_MCP_CONFIGPath to a broker config JSON ({rpcUrl, chainId, registry, tenant, ...}) — its presence is what switches the broker from sandbox mode to own mode.
GRANTOR_MCP_PRINCIPAL_KEYThe delegation-root key grant signs with. Overrides the config file's principalKey; own-tenant mode requires one or the other.
GRANTOR_MCP_ADMIN_KEYThe tenant admin key revoke needs for a real on-chain bumpEpoch. Without it, revoke refuses and prints the exact cast send command to run by hand.
GRANTOR_MCP_STATEOverride the broker-held-child state file path (default ~/.grantor-mcp/state.json).

Becoming a tenant is a handful of on-chain transactions, not a signup form: register at chaingrantor.com/register.html (USDC on Base, no account), or run the grantor-onboard kit's create/ enroll verbs to do the same from a script. See docs/deploy/mcp-sandbox.md for the operator side of standing up a shared broker deployment (what this package's own sandbox is), or docs/guide/mcp-broker.md for the full product walkthrough.

The presented-deed caveat

check also accepts an externally-presented capability deed: check({deed, challenge, tool}). This mode needs a caller-paired challenge — the caller must present the exact challenge value its deed was signed against, which this simple broker does not issue or track itself (broker-held mode self-issues its own, as documented above). It exists so a holder identity the broker does NOT control (minted via grant/delegate with a to argument) can present its own deed for a one-off check — it is not a general-purpose relying-party integration.

If you're building a real relying party — an HTTP API, an MCP server with its own tool surface — verifying deeds presented by arbitrary callers, use @grantor/verify's DeedGuard/CapabilityGuard directly. It owns real challenge issuance and tracking, origin binding, and the full deed lifecycle; this package's check tool is a convenience for a broker managing its OWN children, not a substitute.

DENY codes

check never throws — every refusal comes back as {allow: false, code, reason}:

CodeWhat it means
CapabilityDeniedThe deed is genuine and the caller is authenticated, but no grant covers this (tool, action) — wrong resource, or a failed caveat.
UsesExhaustedThis child's local use budget (broker-metered max_uses) is spent.
EpochRevokedThe chain's live delegationEpoch no longer matches what this link was signed against — a bumpEpoch (real or, in the sandbox demo, pre-bumped) invalidated it.
BadDelegationThe delegation chain itself doesn't check out — a link widened past its parent, a signature didn't recover, or the chain's root isn't a currently enrolled agent key.
TenantInactiveThe tenant is neither Active nor Grace on-chain — the billing gate.
RevokedLocallyThis specific child was revoked at this broker (a fast local short-circuit, checked before any chain read).
UnknownChildNo broker-held child with that child_id — check the ID, or that you're pointed at the state file the grant call used.
BadRequestMalformed check call — neither child_id nor both deed and challenge were supplied. Every field on check's schema is optional, so this is reachable from a real MCP call, not just a hand-built one.
ChainAn on-chain read failed (unreachable RPC, etc.) — fails closed, never allows.

See docs/guide/errors.md for the full verifier-wide table these are drawn from.

Also usable from the CLI

Every tool is also a CLI verb, for scripting outside an MCP client:

node src/cli.js grant --tools search --max-uses 3 --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 1
node src/cli.js revoke --child <child_id>
node src/cli.js status

License

SEE LICENSE IN LICENSE.

FAQs

Package last updated on 21 Aug 2026

Related posts