Skip to content

v0.1.0 - control effects, not content

Choose a tag to compare

@pterbsgame-netizen pterbsgame-netizen released this 30 Jul 12:15
· 7 commits to main since this release

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 approve records each tool's
    name, description and input schema into mcp-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 ~/.ssh is 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/list is never blocked, so a poisoned description still reaches the
    model. Pinning stops the effect, not the reading.
  • confirm cannot ask. elicitation/create is the protocol's answer and no
    client seen so far declares it, so at strict a 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.