New:Socket for Asana Is Now Available.Learn more
Sign In

@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, and wrap any stdio MCP server with enforced permissions. No authorization server anywhere.

latest
npmnpm
Version
0.1.8
Version published
Maintainers
1
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.

And it isn't only advisory: grantor-mcp wrap runs any stdio MCP server behind the broker as an enforcing proxy — ungranted tools are denied before the server ever sees the request, granted tools are metered, and an on-chain revocation lands on the wrapped session's very next call. See Wrap any MCP server (enforcing).

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.

30 seconds: watch a denial happen

npx -y @grantor/mcp demo

Wraps a bundled toy server behind a grant for one tool, lets the granted call through, and shows delete_everything getting refused before the server sees it — enforcement is real (broker + on-chain revocation read), only the "agent" is scripted. Then inspect your own server and get a ready-to-paste wrap suggestion:

npx -y @grantor/mcp tools -- <your MCP server command>

The suggestion grants read-like tool names only (a name heuristic — review it before trusting it); everything outside --tools is denied.

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.

Wrap any MCP server (enforcing)

The five tools above are the broker — authority your agents ask about. wrap is enforcement: run any stdio MCP server behind the proxy and ungranted tools are denied before the server ever sees them, granted tools are metered, and revocation lands mid-session.

# one line: bound a server to two tools, 20 uses, one hour
grantor-mcp wrap --tools search,fetch --max-uses 20 --ttl-secs 3600 -- npx some-mcp-server

# or wrap a child you granted (and can delegate/revoke) beforehand
grantor-mcp wrap --child <child_id> -- npx some-mcp-server

What the wrapped client sees: tools/list filtered to granted ∩ available; a denied tools/call answered with an isError result carrying {allow:false, code, reason} (the model reads why); resources/* and prompts/* denied by default — pass --allow-resources to let them through (unmetered; a security posture flag, off on purpose). Everything else passes through untouched. Denies never decrement the use budget.

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 — and, within one broker, atomic and conserving. max_uses/remaining_uses live in this process's state file (~/.grantor-mcp/state.json by default), not on-chain. Inside one broker process the meter holds two invariants: check reserves the use in the same synchronous step as the balance gate (two concurrent last-use calls can never both pass; a denied call refunds; a crash between authorization and the tool's side effect loses a use — it can never double-spend one), and delegate TRANSFERS uses from the parent's live pool (a tree of delegations can never hold more aggregate uses than the root grant; a drained parent refuses to delegate). Across processes the meter is NOT shared or atomic: run one broker per state file. to-mode links (external holders) carry no broker meter at all — their bound is the signed per-path max_uses caveat alone.

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.
ResourceDenied(wrap only, JSON-RPC error -32001) — resources/*/prompts/* are denied by default under wrap; relaunch with --allow-resources to pass them through.

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
node src/cli.js demo                          # the first denial, scripted, in this terminal
node src/cli.js tools -- npx some-mcp-server  # list a server's tools + suggest a wrap

License

SEE LICENSE IN LICENSE.

Keywords

mcp

FAQs

Package last updated on 25 Aug 2026

Related posts