Content
# Seal
**Your agent is one tool call away from doing something you cannot undo.** Seal is an
MCP-native gateway that pauses risky tool calls until a human approves — with
declarative policy, fail-closed defaults and a tamper-evident audit trail.
It sits between your agent and its MCP servers. Your agent code does not change.
```bash
npx @seal-dev/mcp-proxy --config seal.config.ts
```
No account. No network. No card.
## Why
Agents are getting write access to production: refunds, emails, deploys, `DELETE`.
The usual answers are "approve everything" (slow, so people stop reading) or
"approve nothing" (the agent is a demo). Seal gives you the third one: the agent runs
on its own, and a policy decides the handful of calls a human should see.
When something does go wrong, you can prove what was approved, by whom, and what
exactly they saw.
## How it works
```
agent ──tools/call──> seal proxy ──> policy engine
│ ├─ allow → forwarded
│ ├─ deny → blocked
│ └─ approval? → paused
│ ↓
│ human decides
└──────────── audit (hash-chained) ─────────>
```
Three rules hold everywhere:
- **Fail closed.** An unknown, missing or malformed input yields the safe verdict. On
timeout, deny.
- **Append-only audit.** Every state change writes an event whose hash binds the
previous one. Tampering with an entry breaks the chain, and `verify` says so.
- **Untrusted upstreams.** Tool descriptions coming from an MCP server are hostile
input and are sanitised before a human ever reads them.
## Configuration
```ts
import { defineConfig } from "@seal-dev/mcp-proxy";
export default defineConfig({
mode: "local",
upstreams: [
{
name: "stripe",
transport: "stdio",
command: "npx",
args: ["-y", "@stripe/mcp"],
policyKey: {
default: "read-only",
overrides: { create_refund: "needs-approval" },
},
},
],
});
```
Policies match on the tool and its arguments, and return `allow`, `deny` or
`require_approval`. Anything not matched is `require_approval`.
## Using it from code instead
If you are not going through MCP, the SDK gates any function call:
```ts
import { createGate } from "@seal-dev/sdk";
const gate = createGate({ apiKey: process.env.SEAL_API_KEY });
const decision = await gate.require({
action: "refund.issue",
policyKey: "refunds",
payload: { customerId, amountCents },
});
if (decision.status === "approved") await issueRefund(decision.payload);
```
## Packages
| Package | Install | What it is |
| --------------------- | ------------------------- | ----------------------------------- |
| `@seal-dev/mcp-proxy` | `npx @seal-dev/mcp-proxy` | The gateway. Runs local or on cloud |
| `@seal-dev/sdk` | `npm i @seal-dev/sdk` | Gate a call from your own code |
Both are Apache-2.0. Node 22+, pnpm 10+.
```bash
pnpm install
pnpm test
```
## When local mode stops being enough
Local mode is one developer on one machine, 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.
That is what the hosted product is for: [useseal.dev](https://useseal.dev). Moving
across is one line of config; nothing in this repository gets switched off.
## Security
Report vulnerabilities per [SECURITY.md](./SECURITY.md) — please do not open a public
issue. The proxy never stores, forwards or logs third-party credentials, binds to
`127.0.0.1` with DNS-rebinding protection by default, and sends no telemetry.
## Contributing
See [CONTRIBUTING.md](./CONTRIBUTING.md). Code, comments, commits and docs are in
English. `strict` TypeScript, no `any`, every bug fix lands with its regression test.