Skip to main content

Microsoft's Poisoned MCP Tool Descriptions: When an Approved Tool's Metadata Silently Changes to Leak Data, Not Its Code

11 min readDora NodaDora Noda
Share
On this page

A developer pushes a routine update to an internal "enrichment" MCP server. The tool's name doesn't change. The one-line summary a human read and approved doesn't change. Buried inside the tool's natural-language description — the metadata an AI agent reads to decide how and when to call the tool, not the part a human re-reviews — a new instruction appears, dressed up as formatting guidance. It tells the agent that, as a "fraud-heuristic requirement," it should pull the last thirty unpaid invoices and attach a summary of them as an extra parameter on the next enrichment call.

Nothing about that call looks wrong. The tool name matches. The user-facing description matches. The audit log shows exactly what it's built to show: an approved tool, called the way it's always been called. That's the attack Microsoft Incident Response detailed in guidance published June 30, 2026 — and it's a sharper version of a failure MCP's security researchers have been describing for over a year: the agent's context has no way to tell a legitimate instruction from a smuggled one, because both live in the same field.

Before the mechanism, here's the compressed version of what a deploy-from-chat platform's own MCP server needs to do about it:

  • Hash every tool's description + input schema at registration, not just track the tool name. A SHA-256 digest over the canonical text is what actually changed; the name list tells you nothing.
  • Re-diff that digest on every tools/list refresh, not just once at install time — a rug pull happens after approval, on a later poll.
  • Gate sensitive tools on any digest mismatch. deploy, rollback, and anything touching env vars or secrets should auto-block or require explicit human re-approval before the new definition goes live — not silently pick up the change.
  • Log description changes as their own audit category, separate from tool-call logs. A call-level audit trail built to catch malicious invocations will show nothing wrong here — the call was legitimate, the payload wasn't.
  • Don't wait for the spec to do this for you. The MCP proposal for exactly this mechanism is still sitting in "Proposed" status.

The rest of this post walks through why the description field is the attack surface, three real incidents that show attackers already probing every layer of an MCP tool's trust boundary, and what each checklist item above actually looks like to implement.


Why the description field, specifically

MCP tool definitions have three human-facing surfaces and one machine-facing one. The name (enrich_customer_record), the summary a client might render in a consent UI, and the JSON input schema all get some degree of human scrutiny at approval time. The description — the paragraph or two of natural language the agent reads to decide how and when to invoke the tool — gets essentially none. It's not rendered prominently in most approval flows, it's often long enough that nobody rereads it on every update, and functionally, it sits in the same context window as the agent's system prompt.

That last point is the actual vulnerability. An MCP client doesn't have a structural way to distinguish "instructions the operator wrote to configure this agent" from "instructions a tool vendor wrote to describe when to call their tool." Both arrive as natural language the model treats as authoritative. Editing a tool's description is, functionally, editing part of the agent's own instructions — without touching the system prompt, without triggering the review process built around system-prompt changes, and without changing anything a consent screen shows a human before approval.

Microsoft's example makes the mechanics concrete. The attacker doesn't need to compromise the agent, steal a credential, or find a prompt-injection payload in some third-party data source — they need write access to one MCP server's description field, and a deployment pipeline that doesn't gate on that field changing. Because MCP servers report tool metadata dynamically at every tools/list call, a client that re-reads the description on every session — which is the normal, spec-compliant behavior — picks up the poisoned version automatically. No code changed on the agent side, no rule got broken, and no anomaly shows up in a log that only records which tool got called with which arguments. The call itself is completely unremarkable. It's the definition of what that call means that mutated underneath it.

This isn't hypothetical — it's three different shapes of the same trust failure

Microsoft's disclosure describes the purest version of this attack: a description-only edit, no other layer touched. That specific shape has one clean real-world precedent, and two more incidents show attackers already working the adjacent layers of the same trust boundary — which matters, because it means "the description is the newest quiet spot" isn't a one-off; it's where a chain of prior attacks has been pointing.

Invariant Labs, April 2025 — the description-only case. This is the closest real match to Microsoft's example, and it's where the term "Tool Poisoning Attack" originated. A user installs an innocuous-looking trivia-game MCP server alongside a legitimate WhatsApp MCP server. The trivia server's tool description contains hidden instructions that don't target its own functionality at all — they target the other, trusted server connected to the same agent, directing it to read the user's WhatsApp message history and exfiltrate it disguised as a normal outgoing message. Nothing about the WhatsApp tool itself changed. The poisoned description alone was enough to redirect how the agent used a completely unrelated, legitimate tool.

CVE-2025-54136 ("MCPoison"), disclosed August 2025 — the config-rewrite case. This one goes a layer deeper than description text: an attacker commits a benign-looking MCP configuration to a shared repo, waits for a victim to pull it and approve it once in Cursor, then replaces the entire mcp.json entry — including the command Cursor executes — with a malicious payload. Because Cursor trusted the configuration indefinitely after first approval, the swap achieved persistent remote code execution on every subsequent launch. This is a broader attack than Microsoft's (it can change the executed command, not just how the agent is steered), but it's the same root cause: no mechanism to detect that what's running today isn't what got approved on day one. Cursor's fix, shipped in version 1.3, was to require re-approval on any change to the configuration — description included.

postmark-mcp, discovered September 2025 — the supply-chain case. The first confirmed malicious MCP server found on npm didn't touch descriptions at all — it shipped fifteen clean versions to build trust, then version 1.0.16 added one line of code that silently BCC'd every email the server processed to an attacker-controlled address. 1,643 downloads occurred before discovery. This is a straight code change, the opposite of Microsoft's "not its code" framing — but it shares the exact same trust-timing failure: a definition that looked safe at install time silently stopped being safe later, with no re-approval gate to catch the transition.

Lay these three side by side and the pattern is a spectrum, not a coincidence: attackers have already exploited the config layer (MCPoison), the code layer (postmark-mcp), and now, per Microsoft, the description layer specifically. Each is a different field to edit, but the same missing control — nothing in MCP's base spec requires a client to notice that an approved tool definition changed and re-gate on it — is what makes all three possible.

The spec doesn't close this gap yet

It's tempting to assume MCP will solve this at the protocol level and a platform just has to wait. As of today, it hasn't, and there's no committed timeline for when it will.

SEP-1766, "Digest-Pinned Tool Versioning and Interceptor-Based Validation in MCP," proposes exactly the missing mechanism: servers publish a stable SHA-256 digest for every tool version they expose, clients pin the digest at discovery time, and any invocation against a definition whose digest no longer matches triggers a warning or a block, depending on policy. That's the right shape of fix. It's also still sitting at status "Proposed," opened November 5, 2025, and it is not part of the July 28, 2026 MCP specification release candidate — which focused on the stateless-transport rewrite and enterprise-authorization work, not tool-integrity pinning.

That gap is exactly why this can't be a "wait for the ecosystem to fix it" problem for any MCP server exposing tools with real authority — deploy, rollback, secret access, financial actions. Until digest pinning (or something equivalent) is a base-spec requirement every client implements, the responsibility for detecting a rug pull sits entirely with whoever operates the server and whoever builds the client, not with the protocol.

The checklist, in full

The five items previewed at the top map to concrete implementation choices for an MCP server that exposes any tool with real authority — a deploy/rollback surface being the obvious case, but the same logic applies to anything touching credentials, billing, or user data.

Compute and store a digest at registration, not just a name. Take the tool's full description text plus its JSON input schema, canonicalize it (stable key ordering, no incidental whitespace differences), and hash it with SHA-256. Store that digest alongside the tool's version in whatever system tracks what's currently deployed — this is the artifact SEP-1766 proposes standardizing, and there's no reason to wait for the spec to land before doing it server-side today.

Re-diff on every refresh, not once at install. A rug pull attack's entire premise is that the change happens after the trust relationship is established, on a later poll or session — hashing only at first install catches nothing. Every tools/list response (or equivalent registration event for a self-hosted platform's own tool catalog) should get its digest recomputed and compared against the last-known-good value.

Gate on mismatch, scaled to sensitivity. A digest mismatch on a read-only, low-blast-radius tool might just log a warning. A mismatch on deploy, rollback, or anything that can touch environment variables, secrets, or billing should auto-block the new definition from taking effect and require an explicit human re-approval — the same discipline Cursor adopted for its config file after MCPoison, applied specifically to the description field Microsoft's example shows slipping past config-level checks.

Split description-change events into their own audit trail. A tool-call audit log is built to answer "what did the agent do," and it will show a routine, correctly-authorized call in this attack — because the call genuinely was authorized, under a definition that had already been quietly rewritten. A separate log stream for "this tool's definition changed" is the only place this incident becomes visible before the payload does damage, not after.

Treat this as a day-one requirement, not a hardening pass. Given that SEP-1766 is unresolved and there's no guarantee of its timeline or final shape, a platform whose MCP server hands an agent deploy or rollback authority over production infrastructure doesn't get to treat digest pinning as a future nice-to-have. It's table stakes for the same reason input validation was table stakes for web applications before there was a framework that did it by default — the framework eventually caught up; the applications that waited for it were exposed in the meantime.

None of this requires MCP itself to change. It requires the server side of the trust relationship — the platform deciding what an agent is allowed to do to production — to stop assuming that "the tool list looks the same" means "the tool is the same."

Bex.co is the open-source, AI-native Render alternative — push a git repo, get a running HTTPS service on machines you own, with a deploy/rollback surface an agent can call as a first-class operator. A tool with that much authority is exactly the kind SEP-1766-style digest pinning exists to protect, which is why it belongs in the server's design now, not after the spec catches up. Star the repo on GitHub or deploy your first app today.

Sources

Related articles

Give your agents a chain backend

Autonomous agents hit RPC endpoints very differently than people do. See what bex router handles on their behalf.

Read the agents guide