Skip to main content

Drop-in MCP security middleware: audit · policy · human-approval · injection defense

Project description

🛡️ Warden

PyPI Python License: Apache 2.0

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.1.tar.gz (72.0 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.1-py3-none-any.whl (59.2 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: warden_mcp-0.1.1.tar.gz
  • Upload date:
  • Size: 72.0 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.1.tar.gz
Algorithm Hash digest
SHA256 0cefb4de0809678d789450252ce425d894a06209a1ebb1acd6ba207f79ddb57e
MD5 a1608389e65e0b383fc1c5944cc0c6ec
BLAKE2b-256 3ce11e6208c823d54c5cc610f9266ea964d663c6f8d92df8825f1284781ed0c1

See more details on using hashes here.

File details

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

File metadata

  • Download URL: warden_mcp-0.1.1-py3-none-any.whl
  • Upload date:
  • Size: 59.2 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.1-py3-none-any.whl
Algorithm Hash digest
SHA256 03ab4124b78a5fa97940d677bae73008ca47c55ab0bbcc0c701ddcf0ce64b629
MD5 e4dbd4ab36286e7b4ea2be59a596f738
BLAKE2b-256 900ac176671146fabc9115d0e6cdfc79041bb4cafc06ff51d56b7a253aaeace9

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