Seal

Local mode

Run the MCP approval gateway on your own machine with no account, no network and no card — policy, terminal approval and a hash-chained audit log, in-process.

@seal-dev/mcp-proxy sits between an MCP client and an upstream MCP server. It relays every method untouched and pauses tools/call when a policy says a human should look first. Your agent's code does not change — you point the client at the proxy instead of at the server.

In local mode it needs nothing else. No account, no network, no card.

npx @seal-dev/mcp-proxy --config seal.config.ts

A config that does something

Write seal.config.json next to your project. .ts, .mjs, .js and .cjs work too — use defineConfig from the package for type checking.

seal.config.json
{
  "policies": {
    "refunds": {
      "match": {},
      "rules": [
        { "when": [{ "field": "payload.amount", "op": "lt", "value": 100 }], "then": "allow" },
        { "when": [{ "field": "payload.amount", "op": "gt", "value": 10000 }], "then": "deny" },
        { "when": [], "then": "require_approval" }
      ],
      "approvers": { "channel": "dashboard", "timeoutSeconds": 300 }
    }
  },
  "auditPath": "seal-audit.jsonl",
  "upstreams": [
    {
      "name": "payments",
      "transport": "stdio",
      "command": "npx",
      "args": ["-y", "@your/payments-mcp"],
      "policyKey": "refunds"
    }
  ]
}

Small refunds go straight through. Large ones are refused and never reach the upstream. Everything in between stops and asks you:

────────────────────────────────────────────────────────────
Approve tool call `create_refund`?

Arguments:
{
  "amount": 4200
}

[a] approve   [r] reject   (anything else rejects)
>

Only a approves. Every other line — including an empty one — rejects.

Where the prompt appears

In stdio mode your terminal's stdin and stdout are the MCP channel, so the proxy cannot print a question there without corrupting the protocol. It opens the controlling terminal instead (/dev/tty, or CONIN$/CONOUT$ on Windows).

If your MCP client advertised the elicitation capability during initialize, the proxy asks through the client itself and you never see the terminal prompt. Form mode requests no input fields, so no sensitive data is ever solicited.

If there is neither a terminal nor an elicitation-capable client — CI, a daemon, a detached agent — the proxy refuses to start rather than gating every call until it times out. That is the point where the hosted product starts to matter.

The audit log

Every evaluation, request and decision is appended to auditPath as JSON Lines. Each record's hash covers the previous one, so the file is tamper-evident:

npx @seal-dev/mcp-proxy --verify seal-audit.jsonl
# audit chain intact — 6 events in seal-audit.jsonl

Edit or delete a record and the chain breaks; --verify names the first broken sequence number and exits non-zero. This is the same chain the hosted product uses — the code lives in @seal-dev/shared and is shared by both, rather than being two implementations that agree until they do not.

Fail-closed, by construction

  • An unknown, missing or malformed input yields the safe verdict, never the permissive one. A tool that resolves to no policy key is denied, not relayed.
  • A decision that never arrives expires, and an expired request is refused.
  • A config with no policies, or no way to ask a human, refuses to boot.
  • Tool descriptions from the upstream are untrusted — tool poisoning — and are sanitised before display. So are the arguments on your screen: control, bidi and zero-width characters are stripped so a payload cannot move the cursor, repaint the prompt, or hide text behind an escape sequence.

Upstream credentials

Upstream credentials are named by environment variable (envPassthrough for stdio, auth.valueEnv for HTTP) and resolved from the proxy's own environment at startup. They are never sourced from the calling agent, never stored and never logged. A missing referenced variable fails startup rather than silently relaying without auth.

When one machine stops being enough

Local mode is one developer at one terminal, and it is complete for that: policy, approval, audit, verification. It has no server, so there are things it structurally cannot do — a second person approving, an agent running in CI with nobody at the terminal, an auditor who needs more than a JSONL file on your laptop.

Adding a seal block moves the same config to the hosted product. Approvals then arrive over Slack, email or the review dashboard, and the audit trail is hosted and exportable.

seal.config.json
{
  "seal": { "apiUrl": "https://api.useseal.dev", "apiKeyEnv": "SEAL_API_KEY" },
  "asyncMode": "hold",
  "upstreams": []
}

Nothing in the open-source package is switched off when you do. mode is inferred from what the config declares: a seal block means cloud, its absence means local. In cloud mode policies live in the dashboard, so a leftover local policies map is ignored — the proxy warns rather than letting you believe it is still in force.

Source

@seal-dev/mcp-proxy and @seal-dev/sdk are Apache-2.0 at github.com/useseal/seal.

On this page