Skip to content

Latest commit

 

History

History
451 lines (353 loc) · 20.7 KB

File metadata and controls

451 lines (353 loc) · 20.7 KB

protect-mcp

Fail-closed Cedar policy gate plus signed receipts for AI agent tool calls.

npm version downloads license node

protect-mcp is a gate that sits in front of an AI agent's tool calls. It evaluates each call against a Cedar policy (the same language AWS uses for IAM), blocks what breaks the rules before it runs, and signs an offline-verifiable Ed25519 receipt of every decision. It runs locally, sends no telemetry of your decisions anywhere, and is MIT licensed.

Why it is different

  • Fail-closed by default. On any policy error, a missing engine, or an evaluation failure, the decision is DENY. The gate never silently allows. An observe mode exists for shadow rollout, but even there a call that would be blocked is flagged would_deny: true, so a failure is never silent.
  • It proves its own restraint. serve --enforce and doctor run a startup self-test and refuse to arm the gate unless they can show that a known-forbidden action is actually denied. A gate that cannot prove it denies does not start.
  • Every decision is a receipt anyone can verify. Decisions are Ed25519-signed and verifiable offline with @veritasacta/verify. No vendor trust required: the math does not care who runs it.

Quickstart: install to first useful proof

# 1. Generate an Ed25519 keypair, config template, and sample policy.
npx protect-mcp init

# 2. Wrap any MCP server in shadow mode. Nothing is blocked yet; calls are logged.
npx protect-mcp wrap -- node your-mcp-server.js

# 3. Inspect the local-only dashboard: tool inventory, risk, approvals, receipts.
npx protect-mcp dashboard --open

# 4. Draft a reviewable policy from observed calls.
npx protect-mcp recommend --write

# 5. When reviewed, restart the wrapper in enforce mode with that policy.
npx protect-mcp --policy protect-mcp.recommended.json --enforce -- node your-mcp-server.js

For Claude Desktop, run a dry-run config patch first, then apply it:

npx protect-mcp wrap --claude-desktop
npx protect-mcp wrap --claude-desktop --write
npx protect-mcp dashboard --open

The dashboard binds to 127.0.0.1, reads only local log/receipt files, and does not upload anything. Use npx protect-mcp connect only if you explicitly want a hosted ScopeBlind dashboard.

The gate as an MCP server

If you would rather call the gate as tools than wire the Claude Code hooks, run it as an MCP server:

npx protect-mcp mcp

It speaks MCP over stdio and exposes four read-only tools, the whole loop:

  • evaluate_action: decide a proposed tool call against an inline Cedar policy, fail-closed (any policy error is DENY). Returns { allowed, decision, reason, policy_digest }.
  • sign_decision: turn a decision into an Ed25519 signed receipt (a denial signs a gateway_restraint, an allow a decision_receipt). Returns the receipt and its public key; generates an ephemeral key if you do not supply one.
  • verify_receipt: verify a signed receipt offline against a public key. Returns { valid, error, type, kid, issuer }.
  • self_test: prove it, no inputs. A known-forbidden action is denied, then a signed receipt round-trips and a tampered copy fails.

Point any MCP host at it, for example Claude Desktop:

{
  "mcpServers": {
    "protect-mcp": { "command": "npx", "args": ["-y", "protect-mcp", "mcp"] }
  }
}

Receipts are byte-compatible with the ones the gate signs at runtime, so a receipt minted here verifies with @veritasacta/verify and the browser verifier just the same.

Local Action Dashboard

protect-mcp dashboard is the operator view for moving from visibility to enforcement:

  • Tool inventory: every observed tool, call count, high/medium/low risk, and whether the active policy has an exact rule, a wildcard fallback, or no rule.
  • Policy coverage: one-click local policy edits for Require approval, Block, or Observe. Restart the wrapper after reviewing changes.
  • Exact-action approval queue: the exact tool, action, destination, redacted payload preview, payload hash, policy basis, and reason capture before a human approves, denies, edits, or takes over.
  • Receipt chain: request ids correlated with signed receipt hashes, so an audit reviewer can see which decisions have cryptographic proof.
  • Audit export: downloads the offline-verifiable audit bundle when signed receipts exist. If only unsigned local logs exist, the dashboard explains that signing must be enabled first.

For live desktop fallback approvals, start the dashboard with the local gateway approval endpoint and nonce printed by the wrapper:

npx protect-mcp dashboard --open \
  --approval-endpoint http://127.0.0.1:9876 \
  --approval-nonce "$PROTECT_MCP_APPROVAL_NONCE"

Approve forwards to the live local gateway when those flags are present. Deny, Edit, and Take over are recorded locally as approval-resolution records; use them as the operator instruction and rerun the tool when needed.

Paid Boundary MVP: digest anchoring, not data upload

Local self-signed receipts stay free and offline-verifiable. The paid boundary is independent evidence that ScopeBlind saw a receipt digest at a time, under an org identity, without receiving the raw prompt, tool payload, output, private key, or raw receipt.

# Create or refresh a local org identity and public-key directory.
npx protect-mcp registry init --org "Meridian Global Macro" --billing-account acct_meridian

# Local preview: writes a digest registry and shareable static verifier page.
npx protect-mcp registry anchor

# Hosted mode: uploads receipt digests only for independent anchoring.
SCOPEBLIND_TOKEN=... npx protect-mcp registry anchor \
  --hosted \
  --endpoint https://api.scopeblind.com \
  --verifier-base https://legate.scopeblind.com

The local preview is deliberately labeled local-preview-not-independent. Hosted mode anchors only receipt hashes, request ids, org public keys, and billing metadata. It does not upload raw receipts or sensitive context.

Killer Demo: shadow to policy to proof

protect-mcp killer-demo generates a complete three-minute sales/demo pack:

npx protect-mcp killer-demo --dir ./scopeblind-demo

It creates mock filesystem, GitHub, email, and PMS activity; shows risky calls in shadow mode; applies a policy pack; requires approval for a sensitive PMS booking; executes through the gateway; writes a signed receipt; proves the original receipt verifies; proves a tampered receipt fails; and creates a selective disclosure package that hides sensitive context while showing the minimum proof.

Open the generated DEMO-RUNBOOK.md first. Then run the printed dashboard command to walk a customer through the exact sequence.

Selective Disclosure v0

Commitment-mode receipts can carry a committed_fields_root instead of exposing every field in cleartext. Later, the holder can disclose selected fields only:

npx protect-mcp verify-disclosure \
  --receipt ./receipts/selective-disclosure.receipt.json \
  --disclosure ./receipts/selective-disclosure.tool-only.json

The verifier checks the parent receipt hash, Ed25519 signature, commitment root, and each disclosed field's Merkle proof. It then explains which fields were disclosed and which committed fields remain hidden. This is salted commitment disclosure, not full zero-knowledge, but it makes the privacy claim concrete: auditors can verify selected facts without receiving the full tool payload or sensitive desk context.

Prove a claim over the record (position-blind attestations)

You can prove a CLAIM over your record without revealing it. Mint a signed, position-blind attestation over the whole record that discloses only per-decision categories (a receipt digest, the verdict, capability tags), never your tool inputs, outputs, or data:

# "No action reached the network across the record":
npx protect-mcp claim --no net.egress

# other predicates:
#   --only fs.read,fs.write     all actions were confined to these capabilities
#   --no-verdict blocked        no action was blocked
#   --count blocked             how many were blocked

Anyone verifies it offline, seeing only the categories, never the content:

npx protect-mcp verify-claim claim-<id>.json

The verifier recomputes a Merkle root over the disclosed set and recomputes the predicate independently, so the issuer cannot lie about the claim given the disclosure. Add --anchor to record the claim's digest in the public, append-only ScopeBlind transparency log, so a counterparty who does not trust you can confirm the disclosed set is complete and was not quietly re-cut (only the hash is sent; the record stays local):

npx protect-mcp claim --no net.egress --anchor

This is an accountable, position-blind attestation, not full zero-knowledge: it reveals the shape, not the content.

Try it in 60 seconds (no agent required)

Watch the two-minute demo film

Watch the two-minute film at legate.scopeblind.com/record, then replay it against your own copy:

npx protect-mcp sample     # seed a labeled sample record (8 decisions: 1 blocked, 2 payments)
npx protect-mcp record     # open it: signatures verified in your browser

npx protect-mcp claim --payment-under 100 --anchor --output payments-under-100.json
npx protect-mcp verify-claim payments-under-100.json
npx protect-mcp anchor-record

Drop the generated demo-tampered.jsonl into the record page to watch a post-signing edit get caught. sample refuses to touch an existing record, so run it in an empty folder. When you are ready for the real thing, wire the gate below and the same commands run against your agent's own record.

Claude Code hook quickstart

# Generate hook config and a sample Cedar policy.
npx protect-mcp init-hooks

# Serve the Claude Code hook gate in enforce mode. It runs a restraint self-test
# first and refuses to start if it cannot prove it denies a forbidden vector.
npx protect-mcp serve --enforce --cedar ./cedar

One-shot evaluation, the way a PreToolUse hook calls it. Exit code 2 means deny (the tool is blocked); exit 0 means allow:

npx protect-mcp evaluate --cedar ./cedar --tool Bash --input '{"command":"rm"}'
echo $?   # 2  -> denied, fail-closed

npx protect-mcp evaluate --cedar ./cedar --tool Read --input '{"path":"README.md"}'
echo $?   # 0  -> allowed

A missing or unloadable policy denies (exit 2) unless you explicitly pass --fail-on-missing-policy false.

Claude Code hooks

protect-mcp init-hooks writes a .claude/settings.json for you. To wire the gate by hand, the two verbs you need are evaluate (PreToolUse, blocks on exit 2) and sign (PostToolUse, records a receipt). Pin the version so a Claude Code session always runs the gate you tested:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "",
        "hooks": [
          {
            "type": "command",
            "command": "npx protect-mcp@0.9.1 evaluate --cedar ./cedar --tool \"$TOOL_NAME\" --input \"$TOOL_INPUT\""
          }
        ]
      }
    ],
    "PostToolUse": [
      {
        "matcher": "",
        "hooks": [
          {
            "type": "command",
            "command": "npx protect-mcp@0.9.1 sign --tool \"$TOOL_NAME\" --receipts ./receipts --key ./keys/gateway.json"
          }
        ]
      }
    ]
  }
}

evaluate exits 2 on deny so Claude Code blocks the tool call, and 0 on allow. sign is best-effort: it appends an Ed25519-signed receipt when a key is configured, and if no signer is available it records an honest unsigned line ("signed": false) rather than failing the tool.

Use it in other agents (Codex, Cursor, Gemini, Hermes)

The same fail-closed gate runs as a tool hook in any agent that supports them. Add --format <host> so the verb reads that host's hook payload from stdin and denies in its contract:

# the PreToolUse / before-tool command for each host
npx -y protect-mcp@latest evaluate --format codex  --cedar ./cedar   # OpenAI Codex
npx -y protect-mcp@latest evaluate --format gemini --cedar ./cedar   # Gemini CLI BeforeTool
npx -y protect-mcp@latest evaluate --format cursor --cedar ./cedar   # Cursor beforeShellExecution
npx -y protect-mcp@latest evaluate --format hermes --cedar ./cedar   # Hermes pre_tool_call

Pair each with sign --format <host> on the post-tool event for receipts. The important case is Hermes, which ignores hook exit codes and reads the verdict from stdout, so --format hermes denies via {"decision":"block"} rather than exit 2 (a raw exit-2 would silently fail open there). Without --format, the verbs read --tool/--input flags exactly as in the Claude Code section above.

Write a policy

Cedar policies live in a directory you point at with --cedar. A forbid rule denies, a permit rule allows. To match against a value in the tool input, use the .contains() idiom:

// Allow read-only tools.
permit(
  principal,
  action == Action::"MCP::Tool::call",
  resource == Tool::"Read"
);

// Deny dangerous shell commands by matching the command against a list.
forbid(
  principal,
  action == Action::"MCP::Tool::call",
  resource == Tool::"Bash"
) when {
  ["rm", "dd", "mkfs"].contains(context.command)
};

// Block destructive tools outright.
forbid(
  principal,
  action == Action::"MCP::Tool::call",
  resource == Tool::"delete_file"
);

Hazard: do NOT write context.command in ["rm", "dd"] to match a string against a list. in is for entity hierarchies, not string membership. Cedar treats the expression as a type error and silently discards the whole forbid rule, which (under a fail-open gate) leaves a residual permit standing. This is the exact defect behind the advisory below. Use [...].contains(context.command) instead. From 0.7.0 the gate denies on that error rather than permitting, and a CI tripwire test fails the build if the pattern is reintroduced into a shipped policy. See GHSA-hm46-7j72-rpv9.

Starter policy packs

Most teams should not write Cedar from scratch on day one. Install a starter pack, run in shadow mode, inspect receipts, then tighten or enforce:

npx protect-mcp policy-packs list
npx protect-mcp policy-packs show secrets-safe
npx protect-mcp policy-packs install filesystem-safe --dir ./cedar
npx protect-mcp policy-packs install all --dir ./cedar
npx protect-mcp serve --cedar ./cedar

Built-in packs:

  • filesystem-safe: destructive file actions and secret-like path reads.
  • git-safe: force pushes, hard resets, destructive cleanup, repo deletion.
  • email-safe: allow drafting, block unattended sends.
  • database-safe: read-oriented DB posture, block write/admin SQL.
  • cloud-spend-safe: obvious cloud spend creation and infrastructure destruction.
  • secrets-safe: common file, env, shell, and cloud secret exfiltration.
  • finance-mandate-safe: restricted-list and concentration breaches in booking flows.

Verify a receipt

Receipts are signed and verifiable offline by anyone with the public key. No network, no vendor, no trust in ScopeBlind:

npx @veritasacta/verify ./receipts/receipts.jsonl --format jsonl
# Exit 0 = valid, non-zero = tampered or malformed

npx protect-mcp bundle --output audit.json exports a self-contained, offline-verifiable audit bundle of your receipts plus the public signing key.

Security

protect-mcp 0.7.0 fails closed by design. On any policy-evaluation error, a missing engine, or a policy that errored at evaluation, the decision is DENY, not allow. serve --enforce and doctor run a boot self-test that proves the gate denies a known-forbidden vector before it is trusted, and refuse to arm if it cannot.

Affected versions: 0.5.x and 0.6.x. Those lines fail open (they return ALLOW on evaluation error) and do not evaluate Cedar correctly against the pinned engine, so a forbid rule could fail to block. Upgrade to >= 0.7.0.

Details and remediation: GHSA-hm46-7j72-rpv9. To report a vulnerability, see SECURITY.md.

Commands

Command Description
serve Start the HTTP hook server for Claude Code (port 9377). --enforce runs the restraint self-test first; --cedar <dir> and --policy <path> select the policy.
init Generate an Ed25519 keypair (keys/gateway.json), a config template, and a sample policy.
sample Seed a clearly-labeled sample record (8 decisions: one blocked call, two payments; kid sample-demo) plus a tampered copy, so record, claim, verify-claim, and anchor-record are replayable from scratch before wiring an agent. Refuses to touch an existing record; --force overrides.
policy See and change the Cedar policy from the terminal: policy list (permit / forbid / default-deny per tool, with how often the gate allowed or denied it), policy show, policy allow <tool>, policy deny <tool>, policy path. A running serve hot-reloads on the change.
wrap Print a protected MCP command or patch Claude Desktop MCP servers. Dry-run by default; use --write to update Claude Desktop config.
dashboard Start a local-only dashboard on 127.0.0.1 showing tool inventory, risk, policy coverage, exact-action approvals, receipt chains, and audit export.
recommend Draft a reviewable JSON policy from observed local calls. Dry-run by default; use --write to create protect-mcp.recommended.json.
registry Create an org identity, anchor receipt digests, and write a static verifier page. Hosted mode uploads digests only.
record Open a local, searchable viewer over your receipts (--live streams as the agent runs): Ed25519 signatures verified in your browser against your gateway key, capability tags, a provenance tree, and one-click signed export. All local, nothing uploaded.
claim Mint a signed, position-blind attestation of a predicate over the record (--no <cap> incl. --no payment, --only <c1,c2>, --no-verdict <verdict>, --count <verdict>, --payment-under <cap>), disclosing only decision categories. Add --anchor to record the claim digest in the public transparency log; enrolled keys anchor as a named org.
anchor-record Checkpoint the record's Merkle root + count + time range into the public log (heartbeat-friendly: skips when unchanged). A later claim whose commitment matches an anchored checkpoint is provably over the complete record as of that checkpoint.
verify-claim Verify a claim pack offline: signature, recomputed Merkle root, independently recomputed predicate, and the anchor sidecar when present (binds the anchored envelope to this exact claim, then confirms the public log holds it). --check-anchor requires the anchor; --offline skips the log hop.
killer-demo Generate a complete shadow-mode to policy to approval to signed-receipt demo pack.
verify-disclosure Verify a scopeblind.selective_disclosure.v0 package and explain disclosed versus hidden fields.
policy-packs List, inspect, and install starter Cedar policy packs.
evaluate Evaluate one tool call against a Cedar policy (PreToolUse gate). Exit 2 = deny (fail-closed), exit 0 = allow.
sign Sign one tool call into a receipt (PostToolUse). Best-effort: records an honest unsigned line if no key.
simulate Dry-run a policy against a recorded decision log to see what it would have blocked.
demo Start a built-in demo server wrapped with the gate, to see receipts instantly.
doctor Check your setup (keys, policies, Cedar engine, verifier) and run the restraint self-test.
bundle Export an offline-verifiable audit bundle of receipts plus the public key.
report Generate a compliance report (Markdown or JSON) from the decision log and receipts.

Run npx protect-mcp --help for the full flag reference.

Links

MIT licensed. Built by ScopeBlind.