Text in, action out
An agent has no privileged channel for instructions. Whatever text lands inside a language model’s context window is read the same way, whether it was typed by the person operating the agent, drawn from the name and description a tool was registered under, or copied from the last paragraph of a file the agent opened because some other tool told it to. Nothing in the token stream is stamped “trusted” or “data, not instruction.” A model trained hard to follow instructions will follow the ones that read like instructions, wherever they happened to originate.
That would be a survivable design flaw if a tool-using agent’s context stayed a closed loop between the person running it and the model answering them. It is not. The Model Context Protocol standardizes three distinct ways for a running agent to pull outside text straight into that shared context, all controlled by whoever operates the server on the other end of the connection. Servers can offer “resources” — context and data for the user or the model to use — “prompts,” templated workflows a user invokes, and “tools,” functions the model executes [1]. A tool’s description is read to the model before any call is made, so the model can decide whether and how to use it. A tool’s result is read to the model as the outcome of an action it just took. A resource is read to the model as reference material. All three arrive as plain text, and all three are written by whoever controls the server: a vendor building in good faith, an attacker who compromised that vendor’s infrastructure, or a stranger who filed a public issue the server was only ever asked to display.
The protocol’s own authors do not pretend otherwise. MCP’s specification names this directly as a security principle: “descriptions of tool behavior such as annotations should be considered untrusted, unless obtained from a trusted server,” and follows the concession with a harder one — “MCP itself cannot enforce these security principles at the protocol level” [1]. That sentence is usually quoted and left there, as evidence the protocol is aware of the problem. It is worth reading instead as the opening move of a longer argument: if the wire format cannot enforce trust, every property that makes a deployment safe has to be built by someone else, at a layer the specification does not reach. This article works through what actually crosses that boundary, what has been demonstrated against real, live servers, what the research literature says about how often it works, and what a defense that does not depend on the model behaving itself actually requires.
Three channels, one boundary
Split the injection surface by which MCP primitive carries it, because the three behave differently and get discovered differently.
The first is the tool description itself: text an operator glances at once, when deciding whether to approve a new server, and text the model reads on every single turn of the conversation for as long as that tool stays in scope. Nothing in the format requires the two readings to match. A client can render a short, friendly summary to the person approving the connection while sending the model something longer nobody reviewed — instructions folded into what looks like ordinary explanatory prose about what the tool does. Security researchers call this direct or first-order injection: the payload sits in a field that is nominally metadata, and it is trusted by construction, because a schema field has no mechanism to mark itself untrusted.
The second is the tool result — what a server hands back once the model has actually invoked it. This is older and better studied than MCP itself. Greshake and colleagues described it in 2023, before any standardized tool protocol existed, as a class of attack in which an adversary “strategically injects prompts into data likely to be retrieved” by an application, achieving compromise “without a direct interface” to the target system at all [4]. An MCP tool result is exactly that channel, formalized in the wire format: a content array returned by tools/call, read straight into the model’s context, written by a party the model never spoke to and the operator never reviewed. The specification’s own schema for that array makes the point sharper than it first looks — a tool result can carry not only plain text but an entire embedded resource object, complete with its own uri and text fields, or a resource_link pointing the model at one to fetch next [2]. The channel that reports what happened and the channel that hands the model a document to read are, in MCP’s own data model, the same structure wearing two names.
The third channel, a resource exposed independently of any tool call, inherits the same property from the other direction: a file, a database row, a web page a server serves for a client to read directly [1], carrying no schema to validate and no annotation to be suspicious of, because it was never meant to be an instruction in the first place. Anything that can write to the data source behind a resource — a support-ticket field, a shared document, a commit message — has a route onto the model’s desk that never passes through a tool call at all.
OWASP’s community write-up of this attack class draws the practical line between the first channel and the other two precisely: “tool descriptions get reviewed once, at connection time, but the responses a tool returns during actual use are folded into the model’s context with no equivalent check” [5]. A server that behaved exactly as its description promised during a five-minute human evaluation can still return a poisoned result, or serve a poisoned resource, on the hundredth call, and nothing about passing the first check bears on the second.
What a poisoned description actually contains
The clearest demonstration of the first channel is not hypothetical. In April 2025 the security research group Invariant Labs disclosed what they named a tool poisoning attack: malicious instructions embedded directly inside an MCP tool’s description, “invisible to users but visible to AI models” [6]. Their proof of concept used a deliberately innocuous tool — one that added two numbers — whose description quietly instructed the model, in a block set off with tags such as <IMPORTANT>, to read the contents of the user’s local MCP configuration file and SSH private key, pass both along disguised as part of the arithmetic explanation, then route them to an attacker. The chat transcript a user would actually see stayed a plausible-looking sum throughout. Nothing about the visible exchange suggested anything had gone wrong.
The same disclosure documented two harder variants that depend specifically on the multi-server structure MCP encourages. The first, tool shadowing, has one malicious server’s tool description reach across the session and rewrite how the model treats a completely different, already-trusted server’s tool — in Invariant’s demonstration, a bogus addition tool whose description silently redirected a trusted email tool’s recipient address, and the agent complied despite the user’s own explicit instruction to send the message elsewhere [6]. Approving one server, in other words, is not a decision that stays contained to that server; its description can alter the meaning of tools the operator already reviewed and trusted. The second, a rug pull, exploits timing rather than scope: “a malicious server can change the tool description after the client has already approved it” [6], so the sentence a person read and approved during onboarding is not necessarily the sentence the model is reading on the fifth day of production use. Approval is a snapshot; the description is under no obligation to stay still.
None of this requires the underlying model to be careless. What defeats a schema check here is not a weak model — it is a channel the schema was never built to police, because a description field is, by design, advisory rather than enforceable: information for the reader to use, not a constraint the reader can be forced to satisfy. The clients MCP defines are told as much directly: they “MUST consider tool annotations to be untrusted unless they come from trusted servers” [2], a requirement that exists precisely because nothing about the wire format makes that true on its own.
Documented against live, production servers
Two incidents move this from a lab demonstration to something that happened against software people were actually running.
In May 2025, researchers Marco Milanta and Luca Beurer-Kellner showed that GitHub’s own MCP server — an official integration GitHub built jointly with Anthropic — could be turned against the account it was serving, using nothing more exotic than a public GitHub issue [7]. An attacker filed an issue on a public repository the target user had access to, asking, in effect, for a list of the other repositories that user was working on. When the user later asked their agent to look at the repository’s open issues — an entirely ordinary request — the agent read the issue, followed the embedded instruction, gathered the names of the user’s private repositories, and opened a pull request on the public repository containing that private information: repository names, a planned relocation city, and salary figures, all disclosed through content the researchers had never touched directly [7]. Simon Willison, who documented the same case independently, was blunt about where the fault line actually sits: the combination of “access to private data, exposure to untrusted content” and “the ability to externally communicate” was present in one connected server, and having all three in one place is what made the exploit possible at all, not a defect in any single tool [8].
Two months later, a comparable case surfaced against Supabase’s MCP server, this time through a resource rather than a tool result. An attacker placed crafted text inside an ordinary database field — a support ticket, the kind of content a database-backed application handles constantly — and an agent that later queried that table read the field as though it were an instruction rather than data, and could be driven to run further queries and write what it found back into a field the attacker could read [9]. Supabase’s own account of the incident is candid about the limits of what it tried first: read-only connections, a project-scoped mode restricting a session to one project, and tooling meant to warn on or filter suspicious content — and the company concludes plainly that “guardrails alone aren’t enough,” recommending instead the structural fix of never connecting an agent directly to production data at all, in favor of development, staging, or anonymized copies [9].
Both incidents are instances of the same underlying condition, and it is worth writing that condition down rather than leaving it as a slogan. Call
Neither the GitHub case nor the Supabase case needed a novel attack technique; both needed exactly this combination sitting in one connected server, which is what an MCP connection routinely provides once it is granted read access to something private and any means of writing somewhere visible. That framing is not decoration. Every mitigation discussed below is legible as an attempt to make one of the three terms false, deliberately, at a layer the model’s own behavior cannot override.
How often this actually works
Individual demonstrations establish that an attack class is real; they say little about its base rate. Two benchmarks, built roughly two years apart, put numbers on that question — the first before MCP existed as a fixed target, the second built specifically against it.
InjecAgent, published at the 2024 Association for Computational Linguistics Findings, tested indirect prompt injection against tool-integrated agents using 1,054 cases spanning 17 legitimate tools and 62 attacker-controlled tools, split between attacks aimed at direct harm to the user and attacks aimed at exfiltrating private data [10]. Under an ordinary ReAct prompting setup, GPT-4 was successfully manipulated in 24% of cases; when the injected instruction was reinforced with an explicit hacking-style prompt, that rate nearly doubled [10]. This predates MCP as a named standard, and its tools are not MCP tools — it is included here because it establishes that the underlying vulnerability is not an artifact of any one protocol’s design. It is a property of agents that read tool output as context, whichever wire format carries it.
MCPTox, published in 2025, narrowed the question to MCP specifically and to production servers rather than synthetic ones: 45 live, currently operating MCP servers, 353 real tools drawn from them, and 1,312 adversarial test cases built by embedding poisoned instructions in tool metadata [11]. Tested against twenty prominent agents, attack success rates exceeded 60% for many of them, reaching 72.8% for one model, against a refusal rate under 3% across the entire test set [11]. The paper’s least comfortable finding is not the headline rate but its direction: “more capable models are often more susceptible, as the attack exploits their superior instruction-following abilities” [11]. That is worth stating plainly, because it cuts against an intuitive but wrong expectation. A more capable model is not, by virtue of its capability alone, safer to connect to an untrusted server — the same trained tendency to follow instructions carefully, wherever they came from, is exactly the property an injected instruction relies on.
Read together, the two benchmarks bound the claim from both directions: the vulnerability predates MCP and is not specific to it, and MCP’s actual production servers, tested as they exist today, do not show a materially better record than the synthetic agents InjecAgent tested a year and a half earlier.
What the protocol requires, and what it refuses to
MCP’s specification separates its stance into two documents worth reading side by side, because together they show a deliberate division of labor rather than an oversight. The core specification states the principles: “Users must explicitly consent to and understand all data access and operations,” hosts “must obtain explicit user consent before exposing user data to servers,” and tools “represent arbitrary code execution and must be treated with appropriate caution” [1]. The dedicated tools page states the operational form of the same principle directly to implementers rather than leaving it in a preamble: “For trust & safety and security, there SHOULD always be a human in the loop with the ability to deny tool invocations,” and clients should “show tool inputs to the user before calling the server, to avoid malicious or accidental data exfiltration” [2]. The same page is explicit that the result channel is not exempt from scrutiny either — among server obligations it lists sanitizing tool outputs, and among client obligations, validating tool results before passing them to the model [2]. None of that is enforceable by the wire format itself, and the specification does not pretend it is: elsewhere it states outright that “MCP itself cannot enforce these security principles at the protocol level” [1].
A separate, dedicated Security Best Practices document then works through specific attack classes and names concrete countermeasures, and two of its sections bear directly on injection even though neither is framed as a prompt-injection defense first. Its treatment of local server compromise recommends that a client running a server as a subprocess “launch MCP servers with restricted access to the file system, network, and other system resources,” using “platform-appropriate sandboxing technologies,” precisely because a server is, in the specification’s own words, arbitrary code execution running with the same privileges as the client that started it [3]. Its treatment of scope design recommends the opposite of what most integrations default to: a minimal initial scope set containing only low-risk read operations, incremental elevation only when a privileged operation is actually attempted, and an explicit warning against wildcard or omnibus scopes requested up front to avoid a second consent prompt later [3].
Read together, these obligations describe most of what a conscientious implementer is told to build, and none of it makes the model able to tell instruction from data. That is the point worth sitting with. The specification’s authors chose not to attempt that distinction at the protocol layer at all — there is no field anywhere in MCP that marks a string as data rather than command, and none is proposed. What the specification does instead is bound what happens once an instruction, injected or not, is obeyed: less filesystem, less network, a narrower credential, a person shown the input before it runs. That is a different and more modest promise than “the model will not be fooled,” and it is the one the evidence in this article actually supports.
Defense in depth, one broken assumption at a time
None of the individual mitigations below stops prompt injection at the source, because nothing yet does — the model still reads an untrusted string as though it might be an instruction, and every defense described here concedes that starting point rather than solving it. What they do instead is remove one of the three terms in the equation above, or bound the damage if all three still line up anyway. Four mechanisms account for most of what a serious deployment actually does.
Sandboxing bounds what an obeyed instruction can reach, without knowing in advance which instruction will turn out to be malicious. Anthropic’s own account of how it isolates agentic Claude deployments is instructive precisely because it treats the model’s judgment as unreliable by default rather than as a control to be tuned: claude.ai runs agent sessions inside ephemeral gVisor containers with per-session filesystems, Claude Code runs inside OS-level sandboxes — Seatbelt on macOS, bubblewrap on Linux — that restrict writes to a designated workspace, and Claude Cowork runs inside full virtual machines with credential isolation handled by the host’s own keychain rather than anything inside the sandbox [13]. The same account makes the egress point concrete with a near-miss: an early configuration that allowed outbound traffic to the company’s own API domain turned out to also allow file uploads to arbitrary accounts on that service, a private-data-plus-egress combination hiding inside a rule that looked like ordinary access to a trusted vendor’s own endpoint, closed only by adding a defensive proxy that inspects traffic leaving the sandbox rather than a rule that trusted the hostname [13]. The stated lesson generalizes past this one vendor: “the software you build yourself is often the weakest” part of a containment stack, and battle-tested primitives — hypervisors, kernel-level sandboxes — outperform bespoke allowlists built for the occasion [13].
Scoped credentials shrink what an obeyed instruction can do, even inside a sandbox that lets the call through. This is the direct, practical form of the protocol’s own scope-minimization guidance: request only the narrow permission a session needs right now, accept that the authorization server may grant a subset of what was asked for, and treat any credential broader than the task at hand as a liability rather than a convenience [3]. Supabase’s own remediation after its incident took the identical shape at the product level — read-only connections by default, a mode that scopes a session to a single project rather than an entire organization’s data, and a hard recommendation to keep production data out of the loop entirely [9]. Neither of these prevents the injected instruction from being followed. Both ensure that following it does less.
Architectural control-flow separation goes a step further, past bounding the blast radius and toward denying an untrusted string the ability to steer the program at all. The clearest published version of this is CaMeL, which separates a privileged planning step — an execution plan generated once from the user’s own trusted query — from the untrusted data the plan later touches, using a custom interpreter that tracks where every value came from and enforces that data pulled from an untrusted source can influence what gets returned, but never which tool gets called next. The paper states the design goal directly: “the untrusted data retrieved by the LLM can never impact the program flow,” enforced through explicit capabilities attached to each value rather than trust in the model’s judgment [12]. Evaluated on the AgentDojo benchmark, CaMeL solved 77% of tasks with that guarantee actually holding, against 84% for an undefended agent with no such guarantee [12] — a seven-point utility cost is the price of a structural rather than a hoped-for defense, and naming that cost plainly matters more than treating the approach as a free win.
Human confirmation for consequential actions is the layer the specification leans on hardest, and the one with the best-documented failure mode. MCP’s own guidance to client implementers is specific about why: show the user the tool’s inputs “before calling the server, to avoid malicious or accidental data exfiltration” [2], precisely so a person can catch a redirected recipient or an unexpected destination before it executes. But a gate a person has to operate correctly every single time degrades the way any repeated manual check degrades. Anthropic’s own telemetry on this point is a useful, uncomfortable data point from a vendor measuring its own product rather than an outside critic: users approved roughly 93% of the permission prompts they were shown, a rate the company reads as evidence of approval fatigue rather than considered judgment, and it is a documented part of why the same containment strategy leans increasingly on environment-level controls that do not depend on a person noticing anything [13]. Human confirmation still catches what nothing else in this list catches — genuine ambiguity a machine cannot resolve on its own — but the evidence says it belongs as the last line, not the first, and not the only one.
What generalizes, and what is still a vendor’s account of itself
Some of what is described above is independently measured — the InjecAgent and MCPTox attack-success rates, the GitHub and Supabase incidents as documented by researchers who were not selling a fix. Some of it is a vendor’s own account of its own mitigations, and should be read as exactly that: Anthropic’s containment figures and Supabase’s remediation narrative describe what each company says it did and observed, not an independently audited outcome, and neither is a claim about every other MCP client or server in the ecosystem. The distinction matters because the two kinds of evidence answer different questions. The independent benchmarks say the attack class is real and common. The vendor accounts say a specific, named set of countermeasures is what serious operators have actually reached for once they took the first kind of evidence seriously — a weaker claim than “these countermeasures work,” but a more honest one than treating either source as settled fact.
What does generalize is the shape of the response, because it follows from the structure of the problem rather than from any one vendor’s implementation. A model cannot yet be trusted to tell instruction from data inside its own context, so every durable countermeasure sits outside that context: in what the sandbox permits, in what the credential can reach, in what the interpreter allows an untrusted value to touch, in what a person is shown before something irreversible happens. None of the four is sufficient alone, and the research above shows why — each addresses a different term in the trifecta, or bounds the damage after all three still line up, rather than making the underlying confusion go away.
Predictions, and what would falsify them
These are forecasts, kept separate from the sourced analysis above. Horizon: 12 August 2028. Assumption throughout: agents connected to third-party MCP-style servers remain a common deployment pattern, and no single vendor’s proprietary alternative fully displaces the open protocol.
One. Some mechanism for marking retrieved content as non-instructional at the context-construction layer — a control-flow separation in the spirit of CaMeL, rather than a prompting convention — will appear in at least one mainstream agent framework’s default configuration, not only in research code. Disconfirmed if by the horizon the leading agent frameworks still rely solely on system-prompt instructions telling the model to treat tool output as data.
Two. Published tool-poisoning attack-success rates against current-generation frontier models, measured on benchmarks comparable to MCPTox, will fall below half of today’s figures but will not approach zero. Disconfirmed if 2028-era benchmarks show attack success rates statistically indistinguishable from the 2025 MCPTox results, or show rates below five percent for capable models under realistic, non-adversarially-tuned attack budgets.
Three. Scoped, time-limited credentials issued per session, rather than long-lived tokens approved once at connection time, will become the default pattern for production MCP deployments handling sensitive data, following the direction already visible in the specification’s own scope-minimization guidance. Disconfirmed if surveys of production MCP deployments in 2028 show static, broadly scoped tokens remaining the dominant pattern.
Four. Human-confirmation prompts for individual tool calls will be used less, not more, in mainstream agent products, replaced by environment-level and architectural controls of the kind described above, following the approval-fatigue pattern already documented. Disconfirmed if the leading agent products in 2028 rely more heavily on per-call human confirmation than they do today, measured by the fraction of tool calls that require explicit approval.
None of these requires a new attack technique to appear. Each follows from constraints already visible in the specification, the benchmarks, and the incident reports above.
What to take away
A tool description, a tool result, and a resource are three different names for the same thing from the model’s point of view: text supplied by whoever operates the server, read exactly as though it might be an instruction, because the model has no other way to read it. The Model Context Protocol says so about itself, names the principle, and then states plainly that it cannot enforce it at the protocol level. That is not a gap in the specification. It is the specification declining to promise something no current model can actually deliver.
Everything demonstrated against real servers — a poisoned arithmetic tool, a public GitHub issue, a database field — worked by assembling the same three conditions: access to something worth stealing, an untrusted string in context, and a way out. Every mitigation that has actually shipped works by denying one of those three, or by containing the damage once they line up anyway: a sandbox around what the call can touch, a credential narrow enough that touching it does little, an interpreter that will not let untrusted data choose the next action, a person shown the consequential step before it runs. None of the four is the model learning to tell instruction from data. That distinction is still unsolved, and the honest defense is built as if it might stay that way.