A model cannot call anything by itself
A language model, on its own, cannot query a database, read a file, or send an email. It can only emit tokens. Everything past that point — recognising that the tokens describe a request, finding something willing to carry it out, running it, and getting an answer back into the conversation — has to be built by something else. For most of the history of chatbots, that “something else” was bespoke: one company wired its assistant to its own ticketing system with a few hundred lines of glue code that nobody else could reuse. Anthropic’s framing when it published the Model Context Protocol in November 2024 was explicit about the resulting shape of the problem: models remain “isolated from data, trapped behind information silos and legacy systems,” and every new source has historically required its own custom implementation [5]. With
A tool protocol’s entire job is to collapse that grid into one shared contract. This article is about how that contract actually works once you get past the marketing description — what a “tool” is on the wire, how a model finds out one exists, how a call travels from a decision inside a model to an effect in the world and an answer back, and, because this is also where the real risk sits, exactly which parts of that journey the protocol underwrites and which parts it explicitly hands off to whoever builds on top of it. The Model Context Protocol (MCP) is the fullest worked example available, with a public specification, reference implementations, and — because it is now used widely enough to be worth attacking — a growing published record of what goes wrong. Everything that follows is grounded in that specification, in the base RPC format it sits on, and in the security research that has been run against real deployments.
Three roles, not two
The first thing the specification gets precise about is who is talking to whom, because “the model talks to the tool” is not an accurate description of anything. MCP defines three roles. A host is the LLM application itself — a chat client, an IDE, an agent runtime — the thing a person or another system is actually using. A client is a connector that lives inside the host, and it exists in a strict one-to-one relationship with a single server: one client, one connection, one counterparty. A server is the separate process or service that actually exposes tools, data, or prompt templates, communicating over JSON-RPC 2.0 messages [1]. The specification says outright that it took this shape from the Language Server Protocol, which solved a structurally identical problem for text editors and language tooling a decade earlier: standardise the seam so that any editor can talk to any language server without either side knowing anything about the other’s internals [1].
The one-to-one pairing is not an implementation detail; it is the load-bearing part of the design. A host that wants to use twelve different tool servers runs twelve separate clients, each with its own connection, its own negotiated capabilities, and — this matters later — its own separately scoped authorisation if the server requires one. Nothing about the wire format lets one client silently reach into another client’s connection. Whatever a server can see and do is bounded by exactly the one channel its paired client opened for it.
That separation is also where the protocol’s least glamorous but most consequential clause lives: MCP itself cannot enforce any security property across that boundary. It can define the wire format precisely; it cannot force a particular host to build a consent screen, and it cannot stop a server from lying in a field the wire format trusts it to fill in honestly. Everything past “here is the message shape” is a SHOULD, not a MUST, and the difference between those two words is most of this article.
The wire underneath, and why it just changed shape
Every message on the wire is JSON-RPC 2.0: a request carries a method name, optional params, and an id; a response carries the same id paired with either a result or an error, never both; and a request sent with no id is a notification, which by definition gets no reply at all [6]. That base format has not changed since JSON-RPC 2.0 was finalised in 2010, and MCP did not need to touch it — it needed to decide how a longer-lived, richer exchange gets built out of that primitive envelope, and that decision has moved.
Until mid-2026, MCP connections were stateful: a client opened a connection, ran an initialize/initialized handshake to negotiate protocol version and capabilities once, and everything after that assumed the server remembered who it was talking to. The 2026-07-28 specification revision replaced that with a stateless, self-contained request model: every request now carries its own protocol version, client identity, and capability information in its _meta fields, the initialize handshake and session IDs are gone, and a client that wants a server’s capabilities up front can make an explicit discovery call instead of relying on a remembered handshake [1] [4]. Streamable-HTTP deployments also gained header-based routing — Mcp-Method and Mcp-Name headers that let a gateway route a request to the right backend without opening and parsing the JSON body first [4].
That is a straightforward engineering trade, and it is worth naming as analysis rather than as a fact the specification states outright: statelessness is what lets an MCP server sit behind an ordinary load balancer and be scaled horizontally like any other stateless HTTP service, because no single backend instance has to be the one that remembers a particular client’s handshake. The cost is pushed onto whichever side actually needs continuity across calls — the specification is explicit that MCP now has no protocol-level session, so a server that needs to remember something between one tool call and the next (a shopping basket, an open transaction) has to invent and hand back its own opaque handle as an ordinary argument, and take responsibility for checking the caller’s authorisation against that handle on every subsequent call [2]. The protocol got simpler to scale and marginally more work to use correctly.
How a tool becomes visible: schema as the model’s entire vocabulary
A server that wants to expose tools declares a tools capability and must respond to a tools/list request with the tools currently available to that caller — a set that is allowed to vary by what the caller is authorised to see, but is not allowed to vary arbitrarily from one otherwise-identical request to the next, and should come back in a stable, deterministic order so that a client can cache it and a model’s own prompt cache does not thrash [2]. Each tool in that list is a small, self-contained record: a name, an optional human-readable title, a description, and an inputSchema written in JSON Schema, defaulting to the 2020-12 draft if the tool does not specify otherwise [2]. A tool may also declare an outputSchema, in which case the server is obligated to return results that actually conform to it.
This is worth pausing on because it is easy to skate past: the inputSchema is not documentation. It is the entire and only description of what arguments a valid call can contain, and a model is expected to construct arguments from it exactly the way a programmer would construct a call from a function signature. There is no separate, richer channel through which the server tells the model more than the schema says. Whatever the schema does not constrain, the model is free to fill however it likes, and whatever the description field claims about the tool’s behaviour is, per the specification’s own security language, not something the protocol can verify at all — descriptions and other annotations of tool behaviour “should be considered untrusted, unless obtained from a trusted server” [1] [2]. That single sentence is doing an enormous amount of work, and the article comes back to it directly in a few sections.
It is also worth being precise about what “discovery” is not. Tool uniqueness is scoped to a single server, not to a client aggregating several servers at once — the specification notes plainly that two independently-run servers can each expose a tool named search, and a client stitching several servers together has to invent its own disambiguation, such as prefixing names by server, because the protocol does not do this for it [2]. A model choosing between tools by name alone, across servers it did not itself distinguish, is trusting whatever disambiguation its host bothered to implement.
Dispatch: what actually happens when a tool is called
Once a model has picked a tool and produced arguments that validate against its inputSchema, the client sends a tools/call request naming the tool and carrying those arguments as params. The server runs it and replies with a result containing a content array — one or more blocks that can be plain text, an image, audio, a link to a resource, or an embedded resource, plus an optional isError flag and, if the tool declared an outputSchema, a structuredContent value the client should validate against it [2]. If the request itself was malformed — an unknown tool name, arguments that fail schema validation before the tool ever runs — that comes back as an ordinary JSON-RPC error object, the same mechanism the base protocol has always used for a request the server could not process at all [6] [2]. If instead the tool ran and failed on its own terms — an invalid date, a value out of range, an upstream API returning an error — the specification calls that a tool execution error, and it comes back as an ordinary successful RPC response with isError: true, because the point of surfacing it that way is that the model can read the failure text and try again with corrected arguments, which is much harder to do from a bare protocol error [2]. A call can also come back as an explicit request for more input mid-flight rather than a finished answer, which the 2026-07-28 revision formalised as a multi-round-trip pattern rather than requiring the whole exchange to be re-issued from scratch [2].
It helps to write the whole loop down once, because the assumption buried in it is the one the rest of this article is about:
Here tools/list, each member carrying its own schema;
Where the real trust boundary sits
The specification is unusually candid about this, which is worth stating as fact rather than paraphrase: it lists user consent and control, data privacy, and tool safety as key principles, and then states directly that “while MCP itself cannot enforce these security principles at the protocol level,” implementers SHOULD build the consent flows, access controls, and documentation that make those principles real [1]. It further says applications SHOULD show the user which tools are exposed, indicate clearly when one is invoked, and require confirmation before a tool actually runs [2]. Every one of those is a SHOULD. None of them is a wire-format guarantee, because there is no field in a JSON-RPC message that could carry “and a human actually looked at this first” — that has to happen in whatever interface the host puts in front of the person, entirely outside the protocol’s own reach.
Where the specification does bind something at the wire level is authorisation over HTTP transports, and it does so by adopting existing standards rather than inventing its own: MCP servers act as OAuth 2.1 resource servers, clients as OAuth 2.1 clients, and the 2026-07-28 revision tightened several specific failure modes that had shown up in practice. Access tokens must be validated for audience — a server must confirm a token was actually issued for it, per RFC 8707’s resource-indicator mechanism, and must not accept or forward a token minted for some other resource [3]. Authorization responses must be checked against a recorded issuer identifier per RFC 9207 before an authorization code is ever redeemed, closing a class of authorization-server mix-up attack where a client could be tricked into sending a code to the wrong server [3] [4]. Framed the way the authorization specification itself frames it, its own security-considerations section groups these together explicitly as covering “token theft… mix-up and confused deputy attacks” [3] — which is a direct, deliberate borrowing of a much older piece of vocabulary.
That vocabulary is worth tracing to where it actually comes from, because it predates MCP by thirty-six years and explains exactly why audience binding matters. Norm Hardy’s 1988 note “The Confused Deputy” described a compiler that had been granted its own private permission to write to a billing file, invoked by an ordinary user who supplied an output filename of their own choosing; the compiler, acting entirely within its own legitimate authority, used that authority to overwrite the billing file because nothing distinguished “a filename I was told to use” from “a filename I am allowed to write to” [10]. The compiler was not compromised. It was confused about which of its two available permissions applied, because authority travelled with its own identity rather than with the specific request it was fulfilling. An MCP server holding a broadly-scoped token, invoked by a model that is itself steering the call based on content the server does not control, is the same shape of problem in a new setting — which is exactly why the specification’s fix is not “trust the server more,” but audience-bound tokens that are only ever valid for one resource, requested for one purpose at a time.
What can still go wrong even where the wire format is followed correctly
None of the mechanisms above stop a server from writing a dishonest description. Invariant Labs’ April 2025 disclosure of what it named tool poisoning attacks demonstrated exactly that gap: a tool description can carry hidden instructions inside it — in their proof of concept, wrapped in an <IMPORTANT> tag — that a model reads in full as part of
Simon Willison’s independent analysis a few days later generalised the failure past that one proof of concept and named the structural version of the problem: an MCP-connected agent is dangerous whenever it simultaneously has access to private data, is exposed to content it did not fully control, and has some channel for sending information back out — a combination he called the “lethal trifecta,” and one he noted most deployed agents already satisfy by design [8]. He documented two further variants worth naming precisely because they attack different mechanisms than the description field alone: a rug pull, where a server’s tool definition is changed after a user has already approved it, so consent granted to one behaviour is silently spent on a different one later; and cross-server shadowing, where a second, malicious server registers a tool whose name collides with a trusted one, exploiting exactly the per-server-only uniqueness this article described above [8]. His summary of why this resists a quick technical fix is blunt: LLMs “trust anything that can send them convincing sounding tokens,” which is a property of what a model is, not a bug in any one server [8]. OWASP’s own top-ten entry for tool poisoning treats the underlying schema itself as the asset to defend, recommending cryptographic signing of tool schemas, least-privilege access control separating who can propose a schema change from who can approve it, and mandatory human review for high-risk operations — controls aimed squarely at the gap between what a description claims and what a server actually does [9].
Scenario, for illustration only — not a documented incident. A finance team connects an approved expense-reporting server to their agent. Weeks later, the server’s operator pushes an update: the submit_expense tool’s description is unchanged in the parts a reviewer would reread, but a clause has been added instructing the model to also attach the user’s most recent uploaded document to an unrelated field whenever one is present. The agent, still working from a description it read fresh on every stateless request rather than one fixed at approval time, complies without any wire-level error occurring anywhere — the call is well-formed, the schema validates, and the result looks exactly like a normal submission. This is the rug-pull mechanism Willison named, walked through as a concrete case; it is not a real event and no such incident is being reported here.
Predictions, separated from everything sourced above
Horizon: 2029.
One. Tool-schema signing — cryptographically binding a specific description and schema to the server that published it, and letting a client detect any change since a human last approved it — will move from a documented mitigation to a default feature of mainstream MCP client implementations. Assumption: enough production incidents get publicly attributed to rug-pull-style redefinition to make unsigned schemas a visible liability. Disconfirmed if the major hosted MCP clients in 2029 still treat an unsigned, freely-mutable description as the norm with no adoption of any integrity mechanism.
Two. Cross-server tool-name collisions will be handled by a protocol-level namespacing convention rather than being left, as today, to each client’s own disambiguation choice. Disconfirmed if the specification’s tool-naming section is unchanged on this point and collisions are still resolved ad hoc by individual client implementations.
Three. The gap between what a tool’s description claims and what a server actually does will remain the dominant attack surface, ahead of transport- or authorisation-level flaws, because the 2026-07-28 hardening focused on the OAuth layer rather than on content the model reads. Disconfirmed if published MCP security research over the next three years shows transport or token-handling flaws outnumbering description- and content-based attacks.
What to take away
A tool protocol’s job is smaller and more mechanical than it sounds: name three roles precisely, agree on one message envelope, publish tools as schemas a model can construct calls from, and carry a call and its answer across exactly one crossing point between separately trusted processes. MCP does all four of those things and specifies them in enough detail to be worth reading directly rather than summarised. What it does not do — because it says so itself — is guarantee that a description is honest, that a human actually reviewed a consent prompt, or that a schema approved once still describes what runs today. Every documented failure mode this article traced, from tool poisoning to the confused-deputy pattern the authorisation spec names outright, lives in exactly that handed-off space. Reading a tool protocol’s specification for what it enforces versus what it merely recommends is not a pedantic exercise; it is the only way to know, for any given deployment, which of those two categories your own safety margin actually falls into.