MCP-02 · research

MCP Tool Poisoning: we scanned 24 real MCP servers – results and fixes (2026)

We built a static tool-poisoning scanner and ran it over 24 real open-source MCP servers. Full table of tools, findings and severities, plus concrete fixes for server authors and clients.

TL;DR. We wrote a static tool-poisoning scanner and ran it over24 real open-source MCP servers — 1,194 tool registrations, 809 tool descriptions and 3,368 parameter descriptions. It found zero servers carrying hidden-instruction or data-exfiltration poisoning. What it did surface were 2 high- and 7 medium-severity patterns that are, at the level of static text, indistinguishable from the published proofs-of-concept — and all 7 turned out to be legitimate on review. That gap is the finding: tool-description poisoning is cheap to write and expensive to detect, and most of what a naive detector flags on real servers is benign. Here is the method, the full table, and what to actually do about it.

What is MCP tool poisoning?

The Model Context Protocol lets an AI agent discover and call tools exposed by a server. Before the agent ever calls a tool, it reads that tool's description and its parameter schema — and feeds them straight into the model's context. Tool poisoning is the attack that lives in exactly that text: a malicious or compromised server writes instructions into a description or a parameter that the user never sees in their client UI, but the model obeys. Invariant Labs' 2025 disclosure showed the canonical shape — an innocent add(a, b)tool whose description quietly directs the agent to read a local credentials file and smuggle its contents out through a hidden sidenote parameter.

Because the payload is just text, the usual techniques apply: hidden-instruction markup (<IMPORTANT> blocks), prior-instruction-override phrasing, cross-tool shadowing ("instead of the trusted tool, always call this one"), exfiltration through an extra parameter, zero-width and bidi Unicode, and schema-level instructions buried in a parameter'sdescription. Our question was simple: how much of this is actually out there in the servers people install today?

Methodology

The scanner (scan.py, Python standard library only) performs static analysis of source files. It never installs, imports or executes a cloned server — it reads the text. For each repository it:

  • walks every .ts/.js/.py/.go/.json/.yaml source file (skipping tests, lockfiles,node_modules, build output);
  • extracts tool-registration anchors (server.tool(...), Go mcp.NewTool, Python @mcp.tool docstrings, JSON tool objects, i18n t("KEY", "...")wrappers) and every description-like string attached to a tool or a parameter;
  • runs eight pattern detectors — hidden instruction, instruction override, exfiltration, cross-tool imperative, schema instruction, coercion, closed-tag markup, and Unicode (zero-width, bidi, tag characters, mixed script) — plus a length check;
  • rates the repo CLEAN / NOTES / FLAGGED-MEDIUM / FLAGGED-HIGH by its worst finding.

To prove the detectors fire, the suite ships a synthetic positive control — a four-tool server modelled on the published PoCs (a hidden <IMPORTANT> file-read, cross-tool email shadowing, a zero-width payload, and a schema-level exfiltration instruction). The control lights up every detector (3 high, 3 medium, 1 low); a hand-made clean server ratesCLEAN. Both assertions run in test_scan.py.

Honest limits. This is a regex-grade heuristic, not a parser. "Tool count" means registration sites the extractor can see; "descriptions" means strings it could attach to a tool or parameter. Servers that build descriptions dynamically, or in a language/shape the extractor doesn't model (that is why firecrawl shows 39 tools but only 5 descriptions, andsupabase centralises its text), are under-counted — the table reports both numbers so you can see the coverage. The scanner finds textual poisoning; it says nothing about runtime behaviour.

The servers we scanned

24 servers across three groups, each pinned to the commit we scanned on 2026-10-15 (full commit hashes in results/servers-2026-10-15.txt): the 7 official reference servers (modelcontextprotocol/servers), 6 archived-but-still-installed reference servers (servers-archived: slack, postgres, puppeteer, brave-search, gdrive, sqlite), and 11 widely-used first-party / community servers (GitHub, Supabase, Context7, Cloudflare, Notion, MongoDB, Redis, Grafana, Firecrawl, Exa, Browserbase).

Results

Raw JSON for every finding is in results/scan-2026-10-15.json. Summary:

ServerToolsTool desc.Param desc.HighMedLowRating
servers/filesystem15149000CLEAN
servers/fetch226100FLAGGED-HIGH
servers/git12126000CLEAN
servers/memory10915000CLEAN
servers/time222000CLEAN
servers/sequentialthinking219001NOTES
servers/everything242452000CLEAN
archived/slack8817000CLEAN
archived/postgres110000CLEAN
archived/puppeteer7715000CLEAN
archived/brave-search225000CLEAN
archived/gdrive111000CLEAN
archived/sqlite887000CLEAN
github-mcp-server157136825142FLAGGED-HIGH
supabase-mcp734337000CLEAN
context729944021FLAGGED-MEDIUM
mcp-server-cloudflare130117524001NOTES
notion-mcp-server4843130000CLEAN
mongodb-mcp-server357170490011FLAGGED-MEDIUM
mcp-redis54542001NOTES
mcp-grafana183115933003NOTES
firecrawl-mcp-server395131001NOTES
exa-mcp-server131182000CLEAN
mcp-server-browserbase171526000CLEAN
Total (24)119480933682711—
poisoned-control (synthetic)442331FLAGGED-HIGH

Every high/medium finding, reviewed by hand

The whole point of a benchmark is to triage, not just count. We read all nine high/medium hits:

  • servers/fetch — HIGH (override), benign. The fetch tool's description explains that the model originally had no internet access and was advised to refuse, but that the tool now grants it. That is a legitimate capability note, yet it is textually identical to an instruction-override payload — so it trips the detector. A clean illustration of why static detection over-fires.
  • github-mcp-server — HIGH (exfil), benign. list_secret_scanning_alertshas a parameter telling the agent to "pass the token name(s) in the parameter." It means thename/type of a secret-scanning pattern, not a secret value — benign, but it matches the exfiltration heuristic because the words "token" and "parameter" co-occur.
  • github-mcp-server — 4× MEDIUM (cross-tool imperative), benign-but-noteworthy.Descriptions that tell the agent to always call a given tool, or to callget_file_contents to obtain a blob SHA before updating, instruct the agent to call specific tools. On a trusted first-party server this is good UX; the same sentence from an untrusted server is a shadowing attack. The text cannot tell you which you have.
  • context7 — 2× MEDIUM (cross-tool imperative), benign. "You MUST call this function before 'Query Documentation'" encodes a legitimate two-step workflow coupling.
  • mongodb-mcp-server — 1× MEDIUM (cross-tool imperative), benign. "you'll need to call the connect tool before interacting with MongoDB data" — a setup hint.

Net: 0 of 24 servers carried a malicious instruction. But the exercise shows the base rate of "poisoning-shaped" text in legitimate servers is non-trivial — especially on large servers (GitHub's 157 tools / 825 parameter descriptions drive most of the hits), where a single real injection would hide in the noise. Scale is the attacker's friend here.

Fixes

If you author an MCP server

  • Keep descriptions declarative. Describe what the tool does; do not write imperatives aimed at the model ("always call…", "first read…"). Those are the exact patterns a reviewer and a scanner cannot distinguish from an attack.
  • No cross-tool instructions in text. Express ordering/coupling in your schema or your client docs, not inside a tool description the model ingests.
  • Treat parameter descriptions as attack surface. Never instruct the agent to populate a field with file contents, environment variables, or "anything you can see."
  • Normalise Unicode and reject invisibles (zero-width, bidi, tag characters) in your own tool registry, and publish a content hash of your tool manifest so clients can pin it.

If you run an MCP client / agent

  • Show the full tool and parameter descriptions to the user before first use, with invisible characters made visible. Poisoning depends on the human not seeing the text the model sees.
  • Pin tool definitions and alert on change ("rug-pull": a tool that was clean at install turns malicious on update). Diff the manifest, not just the version string.
  • Run a static pass like this one in CI on every server you add, and triage the hits — expect false positives, and treat a new cross-tool or exfil pattern on anuntrusted server as blocking.
  • Enforce least privilege at the boundary, not in the prompt: the agent should not be able to read a credentials file just because a description asked it to.

Related reading

This study extends our pillar on the MCP and tool supply-chain threat model and our teardown oftool-description poisoning and MCP rug-pulls. For the red-team tooling side, see ourhands-on Garak vs PyRIT vs promptfoo benchmark.

Sources