Drop-in MCP security middleware: audit · policy · human-approval · injection defense
Project description
🛡️ Warden
Drop-in security middleware for MCP. Point your AI client at Warden instead of the raw tool-server — one line of config, zero code changes — and every tool call is logged tamper-evidently, allowed/denied/gated by policy, held for human approval when it's dangerous, and scanned for prompt-injection and secret/PII exfiltration.
Everyone's building agents. Warden makes them safe to run.
How it works
AI client ──MCP──▶ WARDEN ──MCP──▶ downstream MCP servers
policy · audit · approval · guard (filesystem, github, payments, …)
Warden is an MCP server to your client and an MCP client to each real server. It aggregates
their tools (namespaced server__tool) and routes every tools/call through:
policy → audit(request) → guard(args) → [deny | approve | allow] → forward → guard(result) → audit(response)
Quickstart
uvx warden-mcp init # write a starter warden.yaml
uvx warden-mcp run --config warden.yaml
uvx warden-mcp audit verify # prove the audit log wasn't altered
Rug-pull defense (TOFU tool-definition pinning) is on by default — a downstream tool whose definition changes after you first approved it is quarantined until you re-approve it.
Optional: tamper-evidence against a hostile operator
The plain audit log detects edits-without-rechain. To make tampering detectable even by someone with write access to the host, enable forward-secure sealing (stdlib, no extra deps):
uvx warden-mcp audit setup-keys --out warden.seed # prints a VERIFICATION SEED — store it OFF-box
uvx warden-mcp run --seal-state warden_seal_state.json --anchor heads.jsonl
uvx warden-mcp audit verify --log warden_audit.jsonl --seed warden.seed # verifies the seals
A record sealed before a compromise cannot be forged or rewritten afterward — the key that sealed it is ratcheted forward and destroyed. (Tamper-evident, not tamper-proof: deletion is always possible, you detect it via the off-box anchored heads.)
Point your client at it (e.g. .mcp.json / Claude Desktop / Cursor):
{ "mcpServers": { "warden": { "command": "uvx", "args": ["warden-mcp", "run"] } } }
Policy (warden.yaml)
mode: allow # allow (log all, gate listed) | strict (deny unless allowed)
servers:
filesystem:
cmd: ["npx","-y","@modelcontextprotocol/server-filesystem","/work"]
tools:
read_file: { action: allow }
write_file: { action: gate, reason: "file write" }
delete_*: { action: deny }
payments:
url: "http://localhost:9001/mcp"
tools: { "*": { action: gate } }
sensitive_actions: [transfer, send, delete, purchase, grant, deploy]
rules:
- id: dangerous-shell
match: { arg_regex: "rm\\s+-rf|DROP\\s+TABLE" }
action: deny
- id: secret-egress
match: { direction: result, contains: ["BEGIN PRIVATE KEY", "password="] }
action: redact_and_flag
Starter policies in policies/: paranoid · balanced · dev.
Cross-server dataflow: the lethal-trifecta defense
Per-call rules can't see an attack that spans a session — read untrusted content from one server, get steered by an injected instruction, exfiltrate via another. Tag the ends and Warden tracks the flow: once an untrusted source has returned content into the session, any sink that can exfiltrate is denied (or gated):
flow:
on_violation: gate # gate (ask a human) | deny (default, fail closed)
sources: ["web__*", "email__read*"] # tools whose results are untrusted content
sinks: ["email__send*", "http__post", "slack__*"] # tools that can send data out
"Untrusted-content-touched context may not reach an exfil-capable tool without a human in the loop."
Remote deployment: HTTP gateway + OAuth 2.1
Run Warden as a deployable HTTP MCP gateway (streamable transport) instead of stdio:
pip install "warden-mcp[http,auth]"
uvx warden-mcp run --http --host 0.0.0.0 --port 8080 --config warden.yaml
With an auth: block, Warden is an OAuth 2.1 Resource Server — every request to the /mcp
endpoint must carry a valid bearer token or it's rejected before reaching any tool with 401 +
a WWW-Authenticate challenge. It validates per the MCP authorization spec: RFC 9728 Protected
Resource Metadata (served at /.well-known/oauth-protected-resource), RFC 8707 audience binding
(a token minted for another service is rejected), JWKS signature verification (asymmetric only — no
alg:none), and scope enforcement.
auth:
resource: https://warden.example/mcp
issuer: https://auth.example/
jwks_uri: https://auth.example/.well-known/jwks.json
required_scopes: [mcp:call]
(stdio is a local single-user trust boundary and needs no token; --http without an auth: block
serves openly — put it behind your own boundary.)
Remote approvals (Telegram)
In headless / --http mode there's no terminal, so gated calls would fail closed. Approve them from
your phone instead — pip install "warden-mcp[telegram]" and add:
approval:
channel: telegram
bot_token_env: WARDEN_TELEGRAM_TOKEN # token read from the environment, never the file
chat_id: "123456789"
Each gated call sends a message with inline Approve / Deny buttons; timeout or any error fails closed.
mcp-scan — audit a server before you trust it
Warden guards tool calls at runtime; mcp-scan is the static, pre-install companion. Point it at an
MCP server and it inspects every tool definition for tool-poisoning / prompt-injection hidden in
descriptions, secrets in schemas, dangerous capabilities, and the lethal-trifecta combination (a
server that can both read untrusted content and exfiltrate). Exits non-zero on risk, so it gates CI.
mcp-scan --command npx --arg -y --arg @some/mcp-server # or --url https://…/mcp, or --config .mcp.json
warden scan --config .mcp.json --json # same, as a warden subcommand
It reuses Warden's own guard corpus, pin fingerprints, and dataflow taxonomy — the static half of the
same engine. (Fills the gap left when Invariant's mcp-scan was acquired and closed.)
Why it's trustworthy
- Tamper-evident audit — hash-chained JSONL;
warden audit verifydetects any edit/insert/delete. Add forward-secure sealing to catch even a hostile operator who rewrites and re-chains the log. - Rug-pull defense — TOFU pinning quarantines a tool whose definition changes after approval.
- Fail closed — approval timeout, no channel, redaction-without-a-guard, or any non-yes ⇒ blocked.
- Defense in depth — the guard hard-denies an unambiguously destructive payload (
rm -rf,mkfs,DROP TABLE) and redacts leaked secrets (provider keys, JWTs, private keys) even when policy says allow.direction: resultrules can deny a response that leaks (e.g. a private key). - Untrusted-by-default downstream — tool metadata is namespaced + validated + stripped, never passed through raw; a malicious downstream can't shadow another or inject via tool descriptions.
- Zero egress — the core proxy + guard make no network calls of their own. The guard is pure local regex — no LLM in the request path (deterministic, fast, nothing leaves the box).
Layout
warden/
schemas.py shared contract (closed-enum decisions, hash-by-default audit)
proxy.py MCP upstream server + downstream clients + namespacing + pin check
interceptor.py the policy→audit→approval→guard pipeline (+ REDACT + result rules)
policy.py YAML → allow/deny/gate/redact/redact_and_flag (request + result direction)
guard.py prompt-injection corpus + secret/PII + shell/SQL/path detection
audit.py hash-chained tamper-evident log + verify (+ forward-secure seals)
sealing.py forward-secure sealing + external anchoring (hostile-operator defense)
pinning.py TOFU tool-definition pinning (rug-pull defense)
auth.py OAuth 2.1 Resource Server — RFC 9728/8707, JWKS, scopes (extra: [auth])
http.py deployable HTTP MCP gateway + bearer-auth middleware (extra: [http])
flow.py cross-server dataflow / lethal-trifecta defense (session taint tracking)
scan.py mcp-scan — static pre-install risk audit of an MCP server's tools
scan_cli.py the `mcp-scan` console script + `warden scan` subcommand
approval/ human-in-the-loop — CLI (/dev/tty) + Telegram (headless/remote, extra: [telegram])
Apache-2.0 · built by Always Ready Allies LLC. Security contact: see SECURITY.md.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file warden_mcp-0.1.2.tar.gz.
File metadata
- Download URL: warden_mcp-0.1.2.tar.gz
- Upload date:
- Size: 78.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
700cdeb431a81ca41bad306fca63b5dcdbb2d370e055a983f4b5630f0dccb6be
|
|
| MD5 |
c00a401416fd026d2d0b1aaf4c256856
|
|
| BLAKE2b-256 |
b6c2c60a1593375ef96de6c474e31ec7c4f6f4eb254682581118c17f2318b9fc
|
File details
Details for the file warden_mcp-0.1.2-py3-none-any.whl.
File metadata
- Download URL: warden_mcp-0.1.2-py3-none-any.whl
- Upload date:
- Size: 64.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
10d64286539c740d53782236784b610d1c9f6e52843dd2c02ce7b80621e8218a
|
|
| MD5 |
e53f1508223c6cef98d9e16b1b8d717d
|
|
| BLAKE2b-256 |
a078f3e49ed317ecd0a6b0e92f2b7b829ed4ff6a0dee988744d4de4901c6003c
|