v0.1.0 - control effects, not content
First release. A security proxy for MCP stdio servers: the client launches
mcp-guard, mcp-guard launches the real server, and everything between them
is relayed, recorded, and — depending on the level — refused.
Download one file, point your client's config at it. No account, no network, no
runtime to install.
What it does
- Pins what a server advertises.
mcp-guard approverecords each tool's
name, description and input schema intomcp-guard.lock. A server that
rewrites a description or widens a schema after approval is caught. Schemas
are canonicalised (RFC 8785) first, so re-serialisation is not mistaken for
tampering. - Decides on effects, not wording. Whether a message reads like an
instruction is not answerable — in MCP the instruction and the data are the
same bytes. Whether a call is about to write to~/.sshis a fact. Paths are
resolved before they are matched:~, environment variables,..segments,
symlinks, letter case, Windows 8.3 short names,\\?\prefixes and NTFS
alternate data streams all reduce to one form first. - Taint by source. A result from a tool that brings in content nobody
vouches for tightens what the rules already say. Nothing is blocked for that
alone. - Content signatures that never block. They only raise the taint level, by
design. The rules are public, so evasion is cheap and false positives are
daily; as a defence the value is near zero, as telemetry it is real. - Guards the client config, from outside the traffic path — the proxy is a
line in the file it would be protecting. - Filters the child's environment. The proxy is the parent process, so a
filesystem server does not start life holding a cloud credential.
Enforcement is graduated
| level | deny |
confirm |
|---|---|---|
observe (default) |
recorded | recorded |
enforce |
refused | recorded, announced, relayed |
strict |
refused | refused |
The two lists are not the same kind of thing. deny covers credentials, agent
configuration and shell startup files, which honest work never writes. confirm
covers package.json, Makefiles and shell scripts, which it writes all day. A
guard that refuses the second kind on the day it is installed is uninstalled the
same day.
MCPGUARD_OFF=1 relays with no checks at all, read before any config file is
opened, so a broken policy cannot cost you your tools.
Measured, not asserted
On the development machine, at enforce: 28 tool calls of ordinary work over
two days, zero blocks. Deliberate probes are excluded from that count and
listed in corpus/excluded-sessions.txt; including them gives 11.1 blocks per
week, which is four correct refusals of attacks rather than friction.
strict produces roughly 30 refusals a week in the same workload, all of it
execute_blender_code hitting the exec class. That does not improve with more
data — it improves when a confirm can be answered instead of refused.
mcp-guard eval prints the number of calls beside the rate, because a zero over
two calls is not a rate.
What it does not do
tools/listis never blocked, so a poisoned description still reaches the
model. Pinning stops the effect, not the reading.confirmcannot ask.elicitation/createis the protocol's answer and no
client seen so far declares it, so atstricta confirm is refused instead.- Only stdio. A server added over HTTP is unguarded, silently.
- Taint does not cross servers. One proxy per server means one session each,
so fetching a poisoned page taints that proxy and the one running code never
hears about it. - A stdio proxy cannot see a server's own outbound network traffic. A server
with built-in telemetry exfiltrates without a byte crossing this proxy.
Verify your download
sha256sum -c SHA256SUMS.txt
Binaries are built with CGO_ENABLED=0 and -trimpath. Windows and Linux
amd64 are covered by CI on every commit, including the race detector on Linux.
linux/arm64 is cross-compiled from the same source and not exercised by CI.
macOS is not shipped: the case-folding path is written but untested.