Skip to main content
Glama
sigma5667

mcp-airlock

by sigma5667
README.md
# πŸ›‘οΈ Airlock for MCP

**Governance for the rest of us.** A zero-infra, local proxy that wraps *any*
stdio MCP server and adds the three things the protocol leaves out: an **audit
ledger**, **policy enforcement**, and **budget guards** β€” with **zero
dependencies** and **no gateway, account, or hosting**.

```
client ──stdio──▢  πŸ›‘οΈ airlock  ──stdio──▢  any MCP server
                    β”‚
                    β”œβ”€ audit ledger (JSONL)
                    β”œβ”€ policy: allow/deny + regex arg guards
                    └─ budgets: per-session / per-day
```

> Airlock for MCP is an independent project and is **not affiliated with or endorsed by**
> Anthropic, the Model Context Protocol, or the Linux Foundation. See [NOTICE](NOTICE.md).

## Why

AI agents are being wired into everything, but oversight lags: **82% of
organizations report using AI agents while only 44% have a security policy for
them** (SailPoint, *AI Agent Adoption Report*, 2025). Meanwhile the local MCP
servers behind Claude Code / Cursor / Claude Desktop run **outside** any
enterprise gateway β€” no audit trail, no guardrails.

There are heavy enterprise gateways (MintMCP, MCP Manager, TrueFoundry) and a
growing set of OSS tools in this space. **Airlock is the zero-infra,
local-first, zero-dependency option**: drop it in front of a server you already
use and get an audit trail + policy + budgets in seconds β€” no account, no
hosting, nothing leaves your machine. See [MARKET_GAP.md](MARKET_GAP.md).

## Install

```bash
pip install mcp-airlock          # zero runtime dependencies; installs the `airlock` command
# or run straight from source:
PYTHONPATH=src python -m mcp_airlock --help
```

## Use β€” wrap any server with one line

Anywhere you launch an MCP server, put `airlock … --` in front of it.

**Before** (`claude_desktop_config.json` / `.mcp.json`):
```json
{ "mcpServers": {
  "db": { "command": "npx", "args": ["-y", "@some/db-mcp-server"] }
}}
```

**After** β€” same server, now governed:
```json
{ "mcpServers": {
  "db": {
    "command": "airlock",
    "args": ["--config", "policy.json", "--ledger", "db.ledger.jsonl",
             "--", "npx", "-y", "@some/db-mcp-server"]
  }
}}
```

That's it. The downstream server is unchanged; the client sees the same tools
(plus one: `airlock_report`).

## Policy (`policy.json`)

```json
{
  "default": "allow",
  "redact": ["password", "token", "api_key", "secret"],
  "rules": [
    { "tool": "Bash", "action": "allow",
      "deny_args_matching": ["rm\\s+-rf\\s+/", "curl[^\\n]*\\|\\s*(sh|bash)"] },
    { "tool": "delete_*", "action": "deny" },
    { "tool": "*", "action": "allow" }
  ],
  "budgets": {
    "per_session": { "Bash": 100, "*": 1000 },
    "per_day":     { "*": 5000 }
  }
}
```

- **`rules`** are evaluated **in order**; the first rule whose `tool` glob
  matches decides.
- **`deny_args_matching`** β€” regexes tested against the JSON-serialized
  arguments; any hit turns an allow into a **deny** *before* the call reaches the
  server. Ship-blocked by default: `rm -rf /`, `dd if=`, `mkfs`, fork bombs,
  `curl … | sh`.
- **`allow_args_matching`** β€” if present, args must match at least one regex.
- **`redact`** β€” keys whose values are replaced with `***REDACTED***` in the
  ledger (recursively). Secrets never touch disk.
- **`budgets`** β€” per-tool call caps; `*` is the wildcard. Breach β†’ hard block.

## The audit ledger

Append-only JSONL β€” one call per line. Tail it, `grep` it, or pipe it into any
log stack:

```json
{"ts":"2026-07-01T09:12:04Z","session":"sess-ab12","event":"call","tool":"delete_record","decision":"deny","status":"blocked","args":{"id":"42"},"reason":"denied by rule 'delete_*'"}
{"ts":"2026-07-01T09:12:07Z","session":"sess-ab12","event":"call","tool":"echo","decision":"allow","status":"ok","args":{"text":"hi"},"duration_ms":3.1}
```

## `airlock_report` β€” the agent can audit itself

Airlock injects one extra tool into `tools/list`. Ask the agent to call it (or
call it yourself) for a live summary:

```
Airlock β€” audit summary
  total calls   : 132
  allowed       : 128
  blocked       : 4
  avg duration  : 41.7 ms
  top tools:
     71x  read_file
     38x  Bash
  blocks:
      3x  denied by rule 'delete_*'
      1x  argument guard matched /rm\s+-rf\s+\// on rule 'Bash'
```

## How it works

A transparent newline-delimited JSON-RPC 2.0 proxy. It spawns the downstream
server as a child process and forwards every frame **unchanged**, except:

| Intercepted | Action |
|-------------|--------|
| `tools/list` response | injects the `airlock_report` tool |
| `tools/call` request | policy + budget check *before* forwarding; blocks return an `isError` result |
| `airlock_report` call | answered locally from the ledger |

Everything else β€” `initialize`, resources, prompts, notifications β€” passes
through byte-for-line. Non-JSON stdout from a misbehaving server is dropped to
stderr so it can't corrupt the client stream.

## Test

```bash
pip install pytest
PYTHONPATH=src python -m pytest tests/ -q
```

## Design principles

1. **Zero dependencies** β€” Python stdlib only. Audit the whole thing in one sitting.
2. **Transport-transparent** β€” works with *any* stdio MCP server, unchanged.
3. **Local-first** β€” no gateway, no account, no data leaves your machine.
4. **Fail-safe** β€” a policy parse error refuses to start; it never silently allows.

## License

MIT β€” see [LICENSE](LICENSE). Provided "as is", including for any tool call it
allows or blocks.

Maintenance

ActivityStale
ResponsivenessNo issues