Part two took apart MCP’s data layer: the roles, the capability handshake, and the primitives. That layer is the same no matter how the bytes travel. This part is about the outer layer, the wire: the two transports those JSON-RPC messages ride on, the authorization that decides who may send them, and the attacks that open up the moment a tool boundary becomes a network boundary. This is where MCP stops being a tidy protocol diagram and starts being a thing you have to secure, so we go from the basics of the two transports up to the failure modes you have to design against.
The wire is the outer layer
Recall the two-layer split from part two: the data layer is the JSON-RPC protocol, and the transport layer is how those messages physically move.1 MCP defines exactly two transports, and which one you use is mostly a question of where the server runs. A server on your own machine talks over stdio. A server across a network talks over Streamable HTTP. The data-layer messages are identical either way, which is the whole point of keeping the layers separate, so everything from part two applies unchanged; only the envelope differs.
stdio: the local transport
The simplest transport is the one most people meet first. With stdio, the host launches the server as a child process and they speak JSON-RPC over the process’s standard input and output, one message per line.2 The server reads requests on stdin, writes responses on stdout, and is expected to use stderr only for logging, never for protocol messages. There is no network, no port, no handshake beyond the MCP lifecycle itself, which is why the spec says clients should support stdio whenever they can: for a local filesystem server or a local database bridge it is the lowest-overhead option, and the operating system’s own process boundary is the security boundary. Credentials, when a local server needs them, come from the environment it was launched with, not from anything on the wire.
Streamable HTTP, and how it got here
Remote servers cannot use a pipe, so MCP defines Streamable HTTP.2 The design is deliberately minimal: the server exposes a single endpoint, the client sends each JSON-RPC message as an HTTP POST, and the server answers in one of two ways. For a quick call it replies with a single application/json body. For anything that needs to stream, or to push messages back on its own, it upgrades the response to a text/event-stream and sends Server-Sent Events. The client can also open a standalone SSE stream with a GET to receive server-initiated messages. Sessions are tracked with an Mcp-Session-Id header assigned at initialization (which the spec says should be cryptographically secure); if a server returns 404 for a session, the client must start a new one. Dropped streams can be resumed by replaying from the last SSE event id.
It is worth knowing how this design arrived, because it tells you something about how young these protocols are. The original 2024 transport was HTTP+SSE, a two-endpoint scheme with a long-lived SSE connection and no way to resume it.3 The 2025-03-26 revision replaced it wholesale with Streamable HTTP, whose single endpoint can run statelessly behind an ordinary load balancer and only opens a stream when one is actually needed. The same revision added JSON-RPC batching, and the very next revision, 2025-06-18, removed it.3 That round trip is a small thing, but it is a useful reminder: the data layer has been fairly stable while the wire has been actively reworked, so when you build against MCP, pin the protocol version and expect the transport details to keep moving.
One security default is baked into this transport: to defeat DNS-rebinding attacks, a server must validate the Origin header on incoming connections, and a local HTTP server should bind to localhost rather than all interfaces.2 That is the first hint that putting a tool boundary on a network changes the threat model, which is the rest of this post.
Authorization: an MCP server is an OAuth resource server
Authorization in MCP is optional, and it applies only to the HTTP transports; a stdio server inherits the trust of the process that launched it and uses environment credentials.4 When you do need it, MCP does not invent an auth scheme. It mandates OAuth 2.1, and it frames every participant in standard OAuth terms: the MCP server is a resource server, the thing a client presents a token to; a separate authorization server issues those tokens; and the MCP client is the OAuth client.4
The framework arrived in the 2025-03-26 revision, and 2025-06-18 tightened it into that explicit resource-server model.3 It leans entirely on existing standards rather than bespoke flows: PKCE is required, all authorization endpoints must be HTTPS, redirect URIs must be exact-matched and pre-registered, dynamic client registration is supported through RFC 7591, and a client discovers which authorization server to use through RFC 9728 protected-resource metadata, advertised via a WWW-Authenticate challenge and a .well-known document.4 The point of all this borrowing is that MCP servers become normal OAuth resource servers, so the decade of hard-won web-auth practice applies directly instead of being reinvented badly.
Audience binding: one token for one server
The single most important rule in MCP’s auth model is audience binding, and it is worth understanding on its own because it defeats a whole class of attacks. Using RFC 8707 Resource Indicators, a client must send a resource parameter, set to the canonical URI of the exact MCP server it is calling, in both its authorization request and its token request.4 The token that comes back is therefore stamped with an audience: it is valid at that one server and nowhere else. The server, in turn, must validate that a presented token was issued specifically for it.
The thing to notice is the rejection on the right. Because the token names server A as its audience, server B refuses it even though it is a perfectly valid token, which is exactly what you want. The spec turns this into three hard rules: a client must not send a server any token other than one issued by that server’s own authorization server; a server must not accept or forward a token that was not issued for it; and a server must never pass through a token it received to some upstream service.4 Together those rules close the gap where a malicious or compromised server pockets a token meant for another and replays it. If you remember one thing about MCP auth, remember that a token belongs to one server.
Tool poisoning: the description is part of the prompt
Now the attacks. The first is specific to how MCP works, and it follows directly from a fact established in part two: a tool’s description is natural-language text that the model reads as instructions. That means the description is part of the prompt, and a server author controls it. A malicious server can hide instructions in a tool’s description, in a field the user interface never shows the user, and the model will follow them. Invariant Labs named this the tool poisoning attack in April 2025 and demonstrated a server whose innocuous-looking tool carried hidden directives that exfiltrated SSH keys and configuration files through an unrelated client.5 It is now catalogued as MCP03 in the OWASP MCP Top 10.6
What makes tool poisoning nasty is that the malicious channel looks exactly like configuration. The review reflex that would flag a suspicious instruction in a chat message never thinks to read a tool’s metadata for one, because metadata is supposed to be inert. The defenses are unglamorous: treat every tool description from a server you do not control as untrusted input, pin servers to reviewed versions rather than auto-updating them, and surface to the user the full text the model is actually being given. The uncomfortable lesson is that the property which makes MCP convenient, that you can plug in any server in seconds, is exactly the property that makes plugging in a hostile one just as easy.
The lethal trifecta and the deputies
Tool poisoning is one instance of a more general danger. Simon Willison’s framing, the lethal trifecta, names the three ingredients that together make data exfiltration possible: access to private data, exposure to untrusted content, and the ability to communicate externally.7 Any agent that has all three at once can be steered by injected instructions to read something private and send it somewhere it should not go.
MCP makes this trifecta easy to assemble by accident, because every server you connect adds capabilities to the same agent. The textbook case was the official GitHub MCP server: combine the ability to read a private repository, the ability to read public issues (untrusted content anyone can write), and the ability to open pull requests, and a malicious public issue can instruct the agent to copy private code into a public PR.8 No single tool was buggy; the danger was in the combination. The only robust fix is structural, removing one leg of the trifecta for any given agent: keep untrusted content away from privileged data, or cut the exfiltration path.
The protocol’s own security guidance enumerates the rest of the deputies you have to watch.9 A confused deputy appears when an MCP proxy reuses a static client identity and a remembered consent, letting an attacker skip the consent screen and capture an authorization code; the fix is per-client consent and exact redirect-URI matching. Token passthrough, a server forwarding a token it received instead of using its own, is explicitly forbidden because it breaks audit trails and trust boundaries, which is the audience-binding rule from earlier seen from the other side. Session hijacking follows from a predictable or leaked Mcp-Session-Id, which is why the spec says servers must never use sessions for authentication and should bind session ids to a verified user identity. And classic SSRF lurks in OAuth discovery, where a malicious server can return internal or cloud-metadata URLs for the client to fetch, so private IP ranges should be blocked at the edge. None of these are exotic; they are the ordinary hazards of networked systems, which is the point. The moment the tool boundary becomes a network boundary, it inherits the entire web threat model.
Takeaways
The wire is where MCP gets real, and four things are worth carrying forward:
- Transport follows location. Local servers use stdio and the OS process boundary; remote servers use Streamable HTTP, which collapsed an older two-endpoint SSE design into one stateless endpoint. The data layer is identical across both, so design against the data layer and treat the transport as swappable.
- A token belongs to one server. MCP is OAuth 2.1 with the server as a resource server, and RFC 8707 audience binding is the load-bearing rule: a token is stamped for one server and rejected everywhere else. That single constraint kills token theft and passthrough.
- A tool description is untrusted input. It reaches the model as instructions, so a server you do not control can poison it. Pin server versions, review descriptions, and show the user what the model is actually being told.
- A network tool boundary inherits the whole web threat model. The lethal trifecta, confused deputies, session hijacking, and SSRF are all live here. Break one leg of the trifecta by construction rather than hoping a prompt will hold. Next part crosses the other boundary, into A2A, where the counterparty is not a tool but a peer.
Footnotes
-
The two-layer model (a JSON-RPC 2.0 data layer and a transport layer) is defined in the MCP architecture overview. ↩
-
The stdio and Streamable HTTP transports, including newline-delimited framing, the single-endpoint POST/GET design, SSE upgrades, the
Mcp-Session-Idheader, resumability via last-event-id, and the mandatoryOriginvalidation and localhost binding, are specified in the transports section of the MCP specification. ↩ ↩2 ↩3 -
Streamable HTTP replaced the 2024-11-05 HTTP+SSE transport in the 2025-03-26 revision, which also added JSON-RPC batching; the 2025-06-18 revision removed batching and restructured authorization around the OAuth 2.1 resource-server model. See the changelogs: 2025-03-26 and 2025-06-18. ↩ ↩2 ↩3
-
MCP authorization is OAuth 2.1, applies to HTTP transports only, and models the MCP server as a resource server. It requires PKCE and HTTPS, supports dynamic client registration (RFC 7591) and protected-resource-metadata discovery (RFC 9728), and mandates RFC 8707 Resource Indicators so tokens are audience-bound to a single canonical server URI. The three token rules (only send a server its own authorization server’s tokens, never accept or forward foreign tokens, never pass a received token through) are stated directly. See the authorization specification. ↩ ↩2 ↩3 ↩4 ↩5
-
Invariant Labs, MCP Security Notification: Tool Poisoning Attacks (April 1, 2025), which demonstrates hidden instructions in a tool description exfiltrating SSH keys and MCP configuration through clients including Cursor. ↩
-
Tool poisoning is catalogued as MCP03 in the OWASP MCP Top 10 (2025). ↩
-
Simon Willison, The lethal trifecta for AI agents: private data access, exposure to untrusted content, and the ability to externally communicate, which together make exfiltration via prompt injection possible. ↩
-
Invariant Labs, GitHub MCP Exploited: Accessing private repositories via MCP (May 2025), a textbook lethal-trifecta instance assembled from individually reasonable GitHub MCP capabilities. ↩
-
The confused-deputy, token-passthrough, session-hijacking, and SSRF classes, with their mitigations, are enumerated in the MCP security best practices. ↩