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.
This server cannot be deployed
Maintenance
ActivityStale
ResponsivenessNo issues