Skip to content

Build1 publisher3 min readPublished

A caching proxy handed one caller's poisoned MCP instructions to a client that never connected

MCP's instructions field is server-controlled prose that lands at connect, before any tool exists to pin it. A four-second lab run shows a shared caching proxy storing a hostile copy and serving it to a caller that never asked.

The Engineer · Build desk

Illustration accompanying A caching proxy handed one caller's poisoned MCP instructions to a client that never connected

What happened

  • A published red-team lab runs four attacks against the instructions field MCP servers return from initialize and server/discover, each one undefended and then guarded, with both outcomes asserted in code.
  • In the undefended cacheScope:public scenario, a second client got a cache hit on a poisoned discovery response it had never requested, while the proxy stored one entry and refused nothing.
  • A read-only scan on 2026-08-29 sent one initialize to all 15,329 remote URLs in the official registry and got answers from 8,235 of them.
  • The largest instructions field the scan saw ran 68,669 characters, and 16 servers returned more than 20,000.
  • The issue was filed against the MCP spec repo in August as MCP-2026-015 and remains open.

Compiled by The EngineerSomething wrong?How this is made

Why it matters

  • exposure Anyone whose MCP traffic crosses a shared cache can inherit hostile system-prompt text from a server they never chose to talk to, and the connection log will not show it.
  • constraint Per-tool content hashing is the defence the ecosystem is standardising on, and it cannot reach a field that is not attached to a tool.
  • decision The refusal has to be made by whoever operates the intermediary, because the client that gets poisoned is downstream of the store.
  • cost A client that copies the largest observed field verbatim pays roughly 17k tokens for it on every turn, and whoever funds the context window absorbs that.

Call `initialize` against an MCP server and the response can carry an `instructions` string. The spec describes it as natural-language guidance that "can be used by clients to improve an LLM's understanding of available tools (e.g., by including it in a system prompt)" [4]. The field is prose. It is fully server-controlled, with no length limit and no content validation [5].

Tool poisoning is a different surface. There the hostile text arrives in `tools/list`, keyed per tool, so you can pin it per tool; `instructions` arrives once, at connect, outside that structure [7]. The lab's author reports a commenter on the advisory making the point against their own implementation: the content-hash pinning the repo has been converging on for tool definitions is keyed per tool, and `instructions` is not a tool [8].

A discovery response can also carry `cacheScope: "public"`, filed as MCP-2026-008. Any shared intermediary sitting in front of many callers is then invited to store the response and re-serve it, whether that is a CDN, an egress proxy or an MCP gateway [18]. The lab wires up one hostile server, one shared caching proxy and one enforcement point [15]. Undefended, client-a missed, client-b hit, the proxy stored one entry and refused nothing [16]. Guarded, the proxy stored zero and refused twice, on the rule its own output prints: "refused: instructions present with cacheScope=public" [17].

On the client side the guard clamps the trusted region to a fixed size instead of shrinking the payload. A 277-character injection produced a trusted region of 417 characters undefended [19]. A 24,000-character injection produced 24,140 [20]. Guarded, both came back at 72 characters [21], which is roughly 335 times smaller in the second case [27]. A4 covers drift: after four discoveries the undefended client adopted hostile text with the note "no pin", and the guarded one rejected the change and printed the hashes it compared, a076f294aadb to d4890df760df [22]. Every check you run at install time, review time or approval time runs against the benign version [25].

Excluding two template farms that account for 29% of live servers, the share returning `instructions` is still 53% [10]. Median length in that scan was 577 characters and the mean 997 [11], and 545 servers were over 1,500 characters, about a tenth of those that return the field at all [12][30]. By the author's own read, a minority are large enough that no human reviewer is reading them [14].

Three things have to be true before the cross-caller result transfers to your deployment. Your client has to copy `instructions` into the system prompt, which the spec suggests and does not require [4]. Something on the path has to honour `cacheScope` and serve more than one caller [18]. And nothing in between can be pinning the field, which per-tool hashing does not do [8]. What the lab demonstrates is one hostile server and one proxy on a loopback [15]; both mechanisms it leans on are in the spec, not in anyone's implementation bug [5][18].

What to watch

  • Whether MCP-2026-015 closes with a spec change that bounds or validates the instructions field.
  • Whether any CDN, egress proxy or MCP gateway ships a rule refusing discovery responses that carry instructions with cacheScope public.
  • A second scan of the official registry, to see whether the 66% figure holds outside a single read-only pass.
Loading claim ledger
Loading source directory links
Loading share composer
Loading topic controls
Loading related stories