@ai-agent-ledger/mcp-gateway
v0.1.0
Published
Policy enforcement, tamper-evident audit trail, and kill switch for any MCP server — a drop-in governance proxy
Downloads
85
Maintainers
Readme
AgentLedger MCP Gateway 🛡️⛓️
Policy enforcement, tamper-evident audit trail, and kill switch for any MCP server. One line in your config.
Put the gateway in front of any Model Context Protocol server and every tool call is policy-checked before it executes, recorded in a SHA-256 hash chain you can verify later, and stoppable mid-session with an HTTP kill switch. The wrapped server needs zero changes — the agent doesn't even know the gateway is there.
MCP client (Claude Desktop / Claude Code / Cursor / your agent)
│ stdio
▼
agentledger-mcp-gateway ← policies · audit chain · kill switch · risk scoring
│ stdio
▼
any MCP server (filesystem, github, postgres, stripe, your own…)Quick start (Claude Desktop / Claude Code / Cursor)
Wrap the server you already use. Before:
{
"mcpServers": {
"filesystem": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-filesystem", "C:/work"]
}
}
}After — same server, now governed:
{
"mcpServers": {
"filesystem": {
"command": "npx",
"args": [
"-y", "@ai-agent-ledger/mcp-gateway",
"--policies", "C:/work/policies.json",
"--ledger", "C:/work/audit.ndjson",
"--dashboard", "4000",
"--",
"npx", "-y", "@modelcontextprotocol/server-filesystem", "C:/work"
]
}
}
}Everything after -- is the original server command, unchanged.
Write policies three ways
policies.json is an array; mix and match:
[
{ "template": "no_delete" },
{ "template": "no_spend_over", "args": [500] },
{
"id": "no-env-reads",
"description": "Block reads of .env files",
"action": "block",
"severity": "critical",
"conditions": [
{ "type": "tool_match", "tools": ["read_file", "read_text_file"] },
{ "type": "arg_contains", "path": "[0].path", "pattern": ".env" }
]
},
{
"description": "Require human approval before any email to an address outside mycompany.com",
"action": "approve_gate",
"severity": "high"
}
]- Templates — built-ins from the SDK:
no_delete,no_spend_over(n),external_email_gate(domain),business_hours_only(start, end),pii_write_alert(channels). - Raw rules — deterministic condition ASTs (
tool_match,arg_contains,arg_gt,time_outside,data_classification, …). No LLM anywhere. - Natural language — compiled once at startup into a raw rule via whichever key you have set:
ANTHROPIC_API_KEY,OPENAI_API_KEY,GROQ_API_KEY, orGEMINI_API_KEY. Zero LLM calls at enforcement time.
A blocked call returns a normal tool error to the agent (isError: true with the policy reason), so the agent adapts instead of crashing.
Kill switch
Start with --dashboard 4000 and you get a local REST + WebSocket dashboard server:
# Halt this session — the very next tool call throws, mid-conversation
curl -X POST http://localhost:4000/runs/<run_id>/kill
# Live-stream every tool call, block, and escalation
websocat ws://localhost:4000/
# Re-verify the hash chain over everything recorded
curl http://localhost:4000/runs/<run_id>/verifyThe gateway prints the exact kill-switch curl (with the session run_id) to stderr at startup. Add --dashboard-token <secret> before exposing the port beyond localhost.
Tamper-evident audit
Every tool call, policy block, and approval gate is appended to an NDJSON ledger where each entry's id is a SHA-256 over the entry including the previous entry's id. Editing, deleting, reordering, or truncating entries breaks verification — and verification tells you where and how:
import { verifyChain } from '@ai-agent-ledger/sdk'
// { valid: false, broken_at: 3, reason: 'link_broken', runs: [...] }All options
agentledger-mcp-gateway [options] -- <child server command> [args...]
--policies <path> policies.json (templates, raw rules, natural language)
--ledger <path> audit NDJSON file (default ./agentledger-audit.ndjson)
--agent-id <id> agent id recorded in ledger entries (default mcp-gateway)
--run-id <id> session run id (default: generated UUID)
--dashboard <port> REST kill switch + WebSocket live feed
--dashboard-token <tok> require X-AgentLedger-Token on dashboard requests
--name <name> server name advertised to the clientProgrammatic use
import { Client } from '@modelcontextprotocol/sdk/client/index.js'
import { AgentLedger, POLICY_TEMPLATES } from '@ai-agent-ledger/sdk'
import { createGatewayServer } from '@ai-agent-ledger/mcp-gateway'
const ledger = new AgentLedger({
agent_id: 'my-gateway',
storage: { type: 'file', path: './audit.ndjson' },
policies: [POLICY_TEMPLATES.no_spend_over(500)],
})
const server = await createGatewayServer({ child: connectedClient, ledger, runId: 'session-1' })
// connect `server` to any transport (stdio, in-memory, …)Try it in 60 seconds
git clone https://github.com/Esammy/agentledger-mcp-gateway && cd agentledger-mcp-gateway
npm install && npm run build
node examples/e2e-smoke.mjs ./sandbox
# → tools listed, write_file BLOCKED by policy, kill switch fired over HTTP,
# hash chain verified — against the real @modelcontextprotocol/server-filesystemWhat this is (and isn't)
- ✅ Enforcement before execution — not observability after the fact.
- ✅ Deterministic rules on the hot path (<2ms) — the only LLM involvement is optional, once, at startup.
- ✅ Framework-agnostic — governs anything that speaks MCP.
- ❌ Not a sandbox: the gateway governs the MCP channel. A malicious server binary can still do whatever your OS lets it do. Combine with OS-level sandboxing for untrusted code.
Resources and prompts pass through untouched (v0.1 governs tools/call).
Built on @ai-agent-ledger/sdk — the same policies, interceptor, and ledger work directly inside LangChain/OpenAI/Anthropic agents without MCP.
License
MIT
