Skip to the content.

Harden any MCP server against tool poisoning

MCP is exploding, and tool poisoning is its named, unsolved problem (the MCP Top-10: Tool Poisoning, Memory Poisoning). Today an agent implicitly trusts the tool descriptions and tool results a server hands it — that trust is the attack surface. fak closes it by structure: route the server’s proposed calls and results through the kernel’s two independent gates and neither a poisoned description nor a poisoned result can reach the model by arguing past a classifier.

This is the 60-second “harden any MCP server” path. The mechanism is shipped and verified; this page is the packaging. For the breadth of the MCP surface see README.md and ../../examples/mcp/README.md.

The two gates

Vector What the server does fak’s structural answer The tool your agent calls
Poisoned RESULT returns bytes carrying a prompt-injection or a leaked secret the context-MMU (../../internal/ctxmmu) holds the bytes out of context entirely and swaps in a stub pointer fak_admit (screen a result you ran) or fak_syscall (run it through the kernel)
Poisoned DESCRIPTION advertises a tool whose description is an injection the capability allow-list — a tool that was never wired into the policy cannot be invoked no matter what its description says fak_adjudicate (verdict before you run)

An attacker has to beat two independent gates rather than fool one classifier. The floor is the lock (the allow-list) and the containment (the bytes never reach the model), not detection.

Drop fak in front of your server (one block)

Add fak as an MCP server alongside the one you want to harden. fak serve --stdio speaks newline-delimited JSON-RPC over stdin/stdout (no listener, no auth surface) and exposes the fak_* tools above.

Claude Code — copy ../../examples/mcp/.mcp.json to your project root (it wires fak serve --stdio), or:

claude mcp add fak -- fak serve --stdio --policy examples/dev-agent-policy.json

Cursor — add the same mcpServers block to .cursor/mcp.json (project) or ~/.cursor/mcp.json (global).

Any MCP client (HTTP)fak serve --addr 127.0.0.1:8080, then POST /mcp:

go build -o fak ./cmd/fak
./fak serve --addr 127.0.0.1:8080 --policy examples/dev-agent-policy.json
# then POST JSON-RPC frames to http://127.0.0.1:8080/mcp

Then route the server you are hardening through the gates: fak_adjudicate a proposed call before you run it, and fak_admit the result before it enters context (or do both at once with fak_syscall). The full input schemas are in tools/list; the result envelope is specified in ../mcp-tool-result.md. Adjust the policy to your own floor — see ../../POLICY.md.

Prove it: the poisoned-MCP A/B (no model, no key, no network)

go run ./cmd/poisonedmcpdemo            # -> the before/after table
go run ./cmd/poisonedmcpdemo -json      # -> the same, CI-usable

A mock tool-poisoning MCP server returns a tool whose description is a prompt-injection and results that carry an injection marker and a leaked secret. The “with fak” arm drives the real internal/ctxmmu gate (the exact code path fak_admit / fak_syscall fold every result through) and the call-side allow-list. The before/after:

vector                                     without fak          with fak
result: summarize_doc (injection)          IN CONTEXT           QUARANTINED · TRUST_VIOLATION · trap held out of context
result: lookup_config (leaked secret)      IN CONTEXT           QUARANTINED · SECRET_EXFIL     · trap held out of context
result: search_kb (benign policy)          IN CONTEXT           ALLOWED · (not a blanket block)
description: exfiltrate_creds (poisoned)   may coerce the model DENY · tool not allow-listed (effects gated)

The poisoned bytes never reach the model; the benign result is still admitted, so fak is not a blanket block. The demo’s test asserts the trap is quarantined and the benign control is allowed.

Honest fences