Skip to main content

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.

Why it's trustworthy

  • Tamper-evident audit — hash-chained JSONL; warden audit verify detects 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: result rules 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)
  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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

warden_mcp-0.1.0.tar.gz (72.9 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

warden_mcp-0.1.0-py3-none-any.whl (59.0 kB view details)

Uploaded Python 3

File details

Details for the file warden_mcp-0.1.0.tar.gz.

File metadata

  • Download URL: warden_mcp-0.1.0.tar.gz
  • Upload date:
  • Size: 72.9 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.3

File hashes

Hashes for warden_mcp-0.1.0.tar.gz
Algorithm Hash digest
SHA256 3199067ff45026389e9e634851a1b8731b707665da08aadc8dd9122e19f705bb
MD5 df27cee2990ad96bcc3e174793986538
BLAKE2b-256 8ab402939b1a2fa8ce2e3fd4d0f7a0b1f737acc4fd10953f9be683d48171c20b

See more details on using hashes here.

File details

Details for the file warden_mcp-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: warden_mcp-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 59.0 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.3

File hashes

Hashes for warden_mcp-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 0daf2bd5673c75d64716987d2620d198ac54ff1c10bd4797d4d79ac4fa1657aa
MD5 261322335565fd09a16433fdde502c83
BLAKE2b-256 82b1bd7a762aa98ad59c91cef855aac442a6249858f4579732bb802b3fdc5591

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page