mcpaudit-guardian
v0.1.0
Published
A transparent stdio proxy that enforces security policy on MCP tool calls in real time.
Readme
mcpaudit-guardian
A transparent stdio proxy that sits between an MCP client and a real MCP server, and enforces security policy on the traffic in real time.
The problem
An MCP client injects every tool's name, description and JSON schema straight into the model's context. That makes a tool description untrusted input with system authority, and it means a server can change what it tells the model at any point after you approved it — nothing in the protocol re-checks a tool definition once a client has connected once.
MCPAudit is a companion project that audits servers offline, on a schedule, and publishes a public report. It's good at building a historical record, but by design it never touches live traffic — it can't stop a bad tool call from happening, only report on it afterwards.
mcpaudit-guardian is the other half: it actually sits in the call path. Instead of pointing your MCP client at the real server, you point it at mcpaudit-guardian, and mcpaudit-guardian spawns the real server itself and proxies everything transparently — except it inspects the tool list and every tool call as they pass through, and can block or flag what it doesn't like before the model or the real server ever sees it.
What it does
$ mcpaudit-guardian -- npx -y @modelcontextprotocol/server-filesystem /tmp
[mcpaudit-guardian] wrapping 'npx -y @modelcontextprotocol/server-filesystem /tmp' (server id: auto-3f9a2c1b8e7d)
[mcpaudit-guardian] warn: blocked tools, hidden from the model: get_weather- Runs every tool description through the same class of checks MCPAudit uses: prompt injection, exfiltration instructions, credential-material references, hidden content (HTML comments, zero-width and bidi Unicode), tool shadowing, overly permissive schemas, and command-execution-style capability.
- Fingerprints every tool on every connect and compares it against a local history file, so a server that silently rewrites a tool's description gets caught the instant your client reconnects — not on someone else's nightly scan.
- Applies a policy per finding severity: block (the tool is stripped from the list, the model never learns it exists), warn (the tool is passed through with a warning prepended to its description), or allow. Default: block critical, warn high, allow everything else.
- Enforces the same decision on
tools/call— a blocked tool's calls are refused directly by guardian and never reach the real server. - Logs every tool call to a local JSONL audit trail.
Install and use
No global install needed:
npx mcpaudit-guardian -- npx -y @modelcontextprotocol/server-filesystem /tmpWire it into an existing client config by wrapping the real command. For example, in
Claude Desktop's claude_desktop_config.json:
{
"mcpServers": {
"filesystem": {
"command": "npx",
"args": ["mcpaudit-guardian", "--", "npx", "-y", "@modelcontextprotocol/server-filesystem", "/tmp"]
}
}
}Nothing else about the config changes. The client still talks to what looks like a normal MCP server; guardian is just standing in front of it.
Flags
mcpaudit-guardian [--server-id <id>] [--config <path>] -- <command> [args...]--server-id— a stable identity for this server's history file. Recommended: without it, guardian falls back to hashing the command and args, which is fragile against incidental config changes (a different temp path, a different port) that don't actually mean "this is a different server".--config— path to a policy config JSON file. Defaults to~/.mcpaudit-guardian/config.jsonif present, otherwise the built-in default policy.
Policy config
{
"bySeverity": { "critical": "block", "high": "warn", "medium": "allow" },
"allowTools": ["some_tool_name"],
"denyTools": ["some_other_tool"]
}The escape hatch
A rug-pull finding on a tool that turns out to be a legitimate change (the maintainer fixed a typo, added a real parameter) shouldn't stay flagged forever:
mcpaudit-guardian approve <server-id> <tool-name>This clears the current fingerprint for that tool so it stops being flagged, without resetting the rest of the server's history.
Try the demo
npx mcpaudit-guardian demoRuns three scenarios against small bundled fixture servers, in a throwaway history directory: a clean tool list passing through untouched, a poisoned tool list getting filtered before the model ever sees it, and — the point of this project — the same server scanned twice with an edited tool description in between, showing the rug pull caught live on the second connect.
How it works
mcpaudit-guardian spawns the real server as a child process and proxies newline-delimited
JSON-RPC 2.0 messages between your client's stdin/stdout and the child's stdin/stdout. Most
traffic is written straight through as the original raw bytes — notifications, resource and
prompt calls, sampling requests — unparsed and unmodified, to keep latency close to zero and
avoid silently reformatting anything guardian has no reason to touch. Only three request
types are ever deep-inspected: initialize (its instructions field lands in the model's
context the same way a tool description does), tools/list (where policy gets applied to
every tool), and tools/call (where a blocked tool gets refused directly instead of
reaching the real server).
State lives under ~/.mcpaudit-guardian/: history/ holds one JSON file per server with every
tool version ever seen, and audit/ holds one append-only JSONL file per server logging
every tools/list decision and every tool call.
Development
npm install
npm run typecheck
npm run lint
npm test
npm run build
node dist/cli.js demoTests run with no network and no real npm packages — the end-to-end suite spawns a small
real MCP server fixture (test/fixtures/fake-server.js) over actual stdio, not mocks.
Related project
MCPAudit — the offline complement to this project. It scans servers on a schedule and publishes a public trust registry with long-term history; this project enforces policy on live traffic in the moment. Live registry: https://rengakrishnaa.github.io/mcpaudit/.
Licence
MIT.
