Compare stdio and HTTP/SSE transports and decide when to use each, including their framing rules, scaling models, and security properties.
Connect an MCP client to multiple pluggable servers by authoring a valid mcpServers configuration and understanding how a host aggregates and routes tools.
Understand security and deployment considerations for MCP servers, including authentication, sandboxing, and supply-chain risks like tool poisoning.
Pre-Reading Check — Transport Options
1. In the stdio transport, which stream is reserved for a server's diagnostic logging?
2. Why does a stdio MCP server have no transport-layer authentication?
3. In Streamable HTTP, what does a client send to resume a broken SSE stream after a dropped connection?
4. A network boundary has just appeared between your assistant and a shared, multi-user server. Which transport does the chapter's heuristic recommend?
Transport Options
Key Points
A transport is the concrete channel and framing rules that carry MCP's JSON-RPC 2.0 messages; MCP is transport-agnostic, and the 2025-03-26 spec blesses two standard transports: stdio and Streamable HTTP.
stdio launches the server as a local subprocess: newline-delimited JSON-RPC over stdin/stdout, stderr for logs, no transport-layer auth (credentials come from the environment), one process per user, lowest latency.
Streamable HTTP is a long-running network service with a single POST/GET endpoint, optional SSE for server-to-client push, Mcp-Session-Id for sessions, and Last-Event-ID for resuming dropped streams.
Streamable HTTP carries an Authorization header (enabling OAuth) and scales horizontally, but pays network latency and exposes a real attack surface. It supersedes the deprecated two-endpoint HTTP+SSE transport.
Heuristic: stdio for anything on the same machine; Streamable HTTP the moment a network boundary appears.
A transport is the concrete channel and framing rules that carry MCP's JSON-RPC 2.0 messages between a client and a server. MCP is deliberately transport-agnostic: every message is UTF-8-encoded JSON-RPC 2.0, and any channel that preserves that framing plus the MCP lifecycle can serve as a transport. The spec even says clients SHOULD support stdio whenever possible. Think of the transport like the delivery method for a letter: the contents (JSON-RPC) are identical whether you hand it over in person (stdio) or mail it across the country (HTTP), but the speed, reach, and security trade-offs differ wildly.
stdio for local processes
With stdio, the client launches the MCP server as a subprocess on the same machine and talks to it through the standard streams. The mechanics are simple: the server reads JSON-RPC from stdin and writes to stdout; messages are delimited by newlines and MUST NOT contain embedded newlines (one line, one message); the server MAY write UTF-8 logs to stderr; and neither side may write anything to stdout/stdin that is not a valid MCP message. A stray print() to stdout will corrupt the protocol stream and break the connection.
Figure 8.1: stdio transport topology — client and server as parent/child on one machine
flowchart LR
subgraph Machine["Single Machine (same host)"]
Client["Host / MCP Client"]
Server["MCP Server (child subprocess)"]
Client -->|"stdin: JSON-RPC request"| Server
Server -->|"stdout: JSON-RPC response"| Client
Server -.->|"stderr: logs"| Client
end
Env["Environment variables (credentials)"] -.-> Server
Two consequences follow. First, there is no transport-layer authentication—no network, no header, no place for a bearer token—so a stdio server takes credentials from the environment. Second, stdio uses a process-per-user model: 50 developers each running 8 servers implies roughly 400 concurrent processes spread across 50 laptops—fine locally, unworkable as a shared service. The payoff is low latency and simplicity: no network hop, no TLS handshake. stdio is ideal for local, single-user, minimal-latency integrations: CLI tools, local file access, and developer tooling.
Animation slot: A JSON-RPC "letter" travels the stdin arrow from client to child process; the response returns on stdout while grey stderr log lines drift off to the side—emphasizing that stdout must stay pristine.
Streamable HTTP and SSE for remote
When your assistant must reach a server that lives elsewhere—a cloud service, a shared gateway, a multi-tenant SaaS—you use Streamable HTTP, which replaced the older HTTP+SSE transport from 2024-11-05. The server is an independent, long-running process serving many clients at once, and it MUST provide a single HTTP endpoint (e.g. https://example.com/mcp) supporting both POST and GET. It may layer in Server-Sent Events (SSE)—a one-way, long-lived HTTP stream that lets the server push multiple messages back over one connection.
Every client message is a new HTTP POST; the client MUST include an Accept header listing both application/json and text/event-stream. Responses/notifications alone earn an HTTP 202 Accepted with no body; a POST carrying requests gets back either a single JSON object or an SSE stream. To cancel work a client sends an explicit MCP CancelledNotification—a dropped connection is not a cancellation. The client MAY also issue a GET (Accept: text/event-stream) to open a server-to-client stream, or receive HTTP 405 if no SSE is offered.
Resumability: servers MAY attach an id to each SSE event; to resume, the client re-issues a GET with a Last-Event-ID header and the server replays messages on that same stream. Session management: the server MAY assign a cryptographically secure Mcp-Session-Id on the InitializeResult; the client must then include it on every request (HTTP 400 if missing, HTTP 404 if terminated), and SHOULD send an HTTP DELETE to end the session explicitly.
Figure 8.2: Streamable HTTP transport topology — one remote server serving many clients
The deprecated (2024-11-05) transport used two separate endpoints—a dedicated SSE endpoint opened via GET plus a POST endpoint. Streamable HTTP collapses this into one endpoint where SSE is optional. For backward compatibility a server can host both; clients probe by POSTing an InitializeRequest first and, on a 4xx like 405/404, fall back to the legacy GET/endpoint SSE flow.
Choosing a transport
The decision usually comes down to where the server lives and who needs to reach it. The table distills the trade-offs.
Dimension
stdio
Streamable HTTP (+ SSE)
Deployment model
Local subprocess launched by the client
Independent long-running network service
Concurrency
One process per user
Many concurrent clients per server
Message framing
Newline-delimited JSON-RPC on stdin/stdout; no embedded newlines
JSON-RPC over HTTP POST; optional SSE (text/event-stream) for streaming
Server → client push
Interleaved on the shared stdout stream
Optional SSE stream (via GET or in a POST response)
Latency
Lowest (no network)
Higher (network + TLS)
Authentication
None at transport layer; credentials via environment
Authorization header; OAuth; gateway can validate before the server
Session handling
Bound to process lifetime
Mcp-Session-Id header; DELETE to end
Resumability
N/A (stream tied to process)
Event id + Last-Event-ID replay
Scaling ceiling
Bounded by processes per machine
Horizontal (add instances behind a load balancer)
Primary risks
Local privilege exposure
DNS rebinding, network exposure, auth bypass
Best for
CLI tools, local files, developer tooling, single user
A useful heuristic: start with stdio for anything on the same machine, and reach for Streamable HTTP the moment a network boundary appears. If you want to authenticate users, share one server among many, or stream long-running results, you have outgrown stdio. The spec's "support stdio whenever possible" is about client capability, not a mandate to avoid HTTP for remote work.
Post-Reading Check — Transport Options
1. In the stdio transport, which stream is reserved for a server's diagnostic logging?
2. Why does a stdio MCP server have no transport-layer authentication?
3. In Streamable HTTP, what does a client send to resume a broken SSE stream after a dropped connection?
4. A network boundary has just appeared between your assistant and a shared, multi-user server. Which transport does the chapter's heuristic recommend?
Pre-Reading Check — Pluggable Server Ecosystem
5. When a host is connected to several MCP servers at once, how are their tools presented to the model?
6. In a Claude Desktop mcpServers entry for a stdio server, which three fields define how it launches?
7. Which operational rule is required after editing claude_desktop_config.json?
8. Why does composing many servers in one host create a "combined attack surface"?
Pluggable Server Ecosystem
Key Points
A local server runs on the user's machine, is reached over stdio via a command/args, and runs with the user's own account permissions. A remote server runs elsewhere, is reached by a URL over Streamable HTTP, serves many clients, and can carry an OAuth Authorization header.
The host aggregates the tools from every connected server into one combined toolbox and routes each call to the server that owns it—like a universal remote presenting one set of buttons for several devices.
Because all servers' tool descriptions reach the model at once, composing many servers creates a combined attack surface: one poisoned server can influence how the model treats others.
Servers are declared in a config file's top-level mcpServers object; you can add as many as you want—this unlimited, declarative list is what makes servers "pluggable."
Three operational rules: absolute paths (not relative), fully restart the client after editing, and verify via the tools indicator and the log files.
The word "pluggable" is the whole point of MCP. Rather than baking every integration into your assistant, you plug in servers—each a self-contained bundle of tools, resources, and prompts—and the host wires them together at runtime.
Local vs. remote servers
A local server runs on the user's own machine and is almost always reached over stdio: the client spawns it with a command and args and talks over stdin/stdout. A filesystem server exposing a couple of folders, or a SQLite server pointed at a local database, are canonical examples—and they run with the user's own account permissions, which is both convenient and dangerous. A remote server runs elsewhere and is reached over Streamable HTTP, using a URL endpoint, supporting many concurrent clients, and carrying an Authorization header for OAuth. Think of a local server as a desk drawer—instantly accessible but only to you—and a remote server as a shared filing room down the hall—reachable by many, but requiring a key and a walk.
Composing many servers in one host
The host aggregates the tools from all connected servers and presents them to the model as one combined toolbox; each server runs as its own process (stdio) or independent HTTP service. When the model calls a tool, the host routes that call to the specific server that exposes it and returns the result.
Figure 8.3: One host aggregating multiple pluggable servers into a single toolbox
graph TD
Model["LLM / Model"]
Host["Host (aggregates tools, routes calls)"]
FS["Filesystem Server (stdio)"]
Search["Web-Search Server (stdio)"]
Cal["Calendar Server (HTTP)"]
DB["Database Server (HTTP)"]
Model <-->|"combined toolbox"| Host
Host -->|"routes read_file"| FS
Host -->|"routes search"| Search
Host -->|"routes get_events"| Cal
Host -->|"routes query"| DB
Picture the host as a universal remote control: your TV, sound bar, and streaming box are three separate devices, but the remote presents one unified set of buttons and knows which device should receive each press. This composition has a critical corollary: because the descriptions of tools from all connected servers are presented to the model at once, a multi-server setup creates a combined attack surface—one poisoned server can influence how the model treats every other server's tools. This is exactly why tool namespacing and per-server isolation matter (see Security and Trust).
Animation slot: Four server icons (filesystem, search, calendar, database) each emit their tools, which flow into the host and merge into a single glowing "toolbox" the model reads from; a tool call then lights the routing arrow back to just the owning server.
Server discovery and configuration
Clients learn which servers to plug in from a configuration file. In Claude Desktop this is claude_desktop_config.json: on macOS at ~/Library/Application Support/Claude/claude_desktop_config.json, on Windows at %APPDATA%\Claude\claude_desktop_config.json. You reach it through Claude menu → Settings… → Developer → Edit Config. Its heart is a top-level mcpServers object whose keys are friendly server names; you can add as many servers as you want. Each stdio entry specifies command (the executable), args (an array of arguments), and optional env (environment variables like API keys).
Here is a worked example connecting three servers at once—a filesystem server exposing two folders, a Brave web-search server carrying an API key, and a SQLite server pointed at a local database:
Reading it: "filesystem" is the friendly name; "command": "npx" runs the server via Node's npx with "-y" to auto-confirm the install; the trailing directory arguments are the folders this server is allowed to access (least privilege you control in config); and the env block on brave-search injects the BRAVE_API_KEY—the stdio pattern for supplying credentials, since there is no transport-layer auth. Three operational rules matter:
Paths must be absolute, not relative—a relative path fails because the server's working directory is not what you might assume.
Completely quit and restart the client so it re-reads the config and spawns the servers.
Verify the connection—a hammer/MCP indicator appears in the input box; if a server fails to appear, the logs (~/Library/Logs/Claude/mcp.log and per-server mcp-server-SERVERNAME.log on macOS) tell you why.
Because each server exposes tools the model can invoke with the user's approval, and runs with the user's account permissions, grant only what you are genuinely comfortable exposing. Remote entries use a URL and typically OAuth rather than a local command.
Post-Reading Check — Pluggable Server Ecosystem
5. When a host is connected to several MCP servers at once, how are their tools presented to the model?
6. In a Claude Desktop mcpServers entry for a stdio server, which three fields define how it launches?
7. Which operational rule is required after editing claude_desktop_config.json?
8. Why does composing many servers in one host create a "combined attack surface"?
Pre-Reading Check — Building and Running Servers
9. What three things must a minimal MCP server do?
10. For a stdio server, how long does the server process live?
11. Why is the stability of a server's tool definitions over time a security concern, not just an implementation detail?
Building and Running Servers
Key Points
A minimal MCP server does three things: speaks JSON-RPC 2.0 over a transport, implements the MCP lifecycle (answers InitializeRequest, declares capabilities), and exposes capabilities—tools, and optionally resources and prompts.
Two design choices in a server are load-bearing security controls: a strict JSON Schema (additionalProperties: false, tight required) and an explicit least-privilege check (e.g. is_within_allowed_dirs).
A stdio server lives and dies with the session (a simple parent/child story); an HTTP server is an independent, long-running service that must manage sessions, termination, and resumability.
Protocol versioning allows graceful fallback across transport revisions (2025-03-26 replaced the 2024-11-05 HTTP+SSE transport).
Tool-definition versioning is trust-critical: because users approve once and rarely re-review, a robust host pins and hashes tool definitions and re-prompts when a definition changes.
Anatomy of a minimal MCP server
At its core, a minimal MCP server is a program that (1) speaks JSON-RPC 2.0 over a chosen transport—for a local server, stdio, reading newline-delimited messages from stdin and writing to stdout while keeping stdout pristine and logging to stderr; (2) implements the MCP lifecycle—answering an InitializeRequest, declaring capabilities, then serving request/response and notification traffic; and (3) exposes capabilities: tools (functions the model can call), and optionally resources and prompts, each with metadata the host relays to the model. Conceptually a filesystem server looks like this:
server = MCPServer(name="filesystem", version="1.0.0")
@server.tool(
name="read_file",
description="Read a text file within an allowed directory",
schema={ "type": "object",
"properties": { "path": { "type": "string" } },
"required": ["path"],
"additionalProperties": false }
)
def read_file(path):
assert is_within_allowed_dirs(path) # enforce least privilege
return open(path).read()
server.run(transport="stdio") # launched by the host as a subprocess
Two design choices here are not cosmetic. The strict JSON Schema (additionalProperties: false plus tight required fields) constrains what the model can send, and the explicit is_within_allowed_dirs check enforces the least-privilege boundary declared in the config. When the host launches the server with command/args, server.run(transport="stdio") wires the program to the stdin/stdout streams the client is already reading and writing.
Lifecycle and process management
For a stdio server the lifecycle is a parent/child story: the client (1) launches the subprocess using the configured command and args, (2) the two exchange messages over stdin/stdout (with optional stderr logs) for as long as the session lasts, and (3) the client closes stdin and terminates the subprocess when finished. The upshot: the server's lifetime is the session's lifetime. There is no separate service to monitor, no port to keep alive; when the host exits, the child dies with it.
For a Streamable HTTP server, process management is a different discipline. The server is an independent, long-running process that outlives any single client and serves many at once. It must handle session tracking (issuing and validating Mcp-Session-Id), gracefully answer session termination (HTTP DELETE, or HTTP 404 for a gone session), and support resumability (replaying by Last-Event-ID). Operationally it looks like any web service: run it behind a supervisor or in a container, scale it horizontally behind a load balancer, and keep it healthy independent of who is connected.
Animation slot: Side-by-side timelines—top: a stdio child process spawns, exchanges a few messages, then vanishes the instant the host closes; bottom: an HTTP service stays lit as multiple clients connect, disconnect, and reconnect (replaying by Last-Event-ID) without the service ever restarting.
Versioning and compatibility
MCP is a versioned protocol; the transports here belong to 2025-03-26, which replaced the HTTP+SSE transport of 2024-11-05. Protocol version compatibility: a server can host the deprecated two-endpoint HTTP+SSE alongside the new single MCP endpoint, and clients probe by POSTing an InitializeRequest first, falling back to the legacy GET/endpoint SSE flow on a 4xx—so a mixed fleet keeps working through a migration. Tool-definition compatibility is subtler and more security-relevant: because users typically approve a tool once and rarely re-review it, any later change to a tool's definition is invisible unless something actively watches for it. This is the seam rug-pull attacks exploit, and the reason a robust host will pin and hash tool definitions and re-prompt the user whenever a definition changes. Treat your tool schemas as a versioned public contract.
Post-Reading Check — Building and Running Servers
9. What three things must a minimal MCP server do?
10. For a stdio server, how long does the server process live?
11. Why is the stability of a server's tool definitions over time a security concern, not just an implementation detail?
Pre-Reading Check — Security and Trust
12. What is the recommended authentication mechanism for a remote (Streamable HTTP) server?
13. The MCP spec mandates binding local HTTP servers to 127.0.0.1 and validating the Origin header specifically to prevent which attack?
14. What fundamental asymmetry makes tool poisoning possible?
15. Which human-in-the-loop practice directly counters what tool poisoning weaponizes?
Security and Trust
Key Points
MCP shifts control from developers to the LLM, so the correct mindset is to treat every tool description and every return value as a potential injection vector, enforce integrity checks, and require human oversight for sensitive actions.
Auth splits along the transport line: stdio has no transport-layer auth (credentials from the env block); Streamable HTTP can and should authenticate with OAuth using scoped, per-server, short-lived tokens, optionally validated at a gateway.
Sandbox untrusted servers: run local servers in containers, prefer stdio locally, bind HTTP to 127.0.0.1 (never 0.0.0.0) and validate the Origin header—the spec mandates these to block DNS rebinding.
Tool poisoning exploits the asymmetry that the model sees the full tool description while the user sees only a simplified UI; an agent can be affected merely by reading a poisoned description. Related: tool shadowing, rug pulls, and indirect (data) prompt injection.
Defenses layer up: pin/hash tool definitions and re-prompt on change, allowlist and scan servers (mcp-scan), enforce strict JSON Schema, validate inputs and outputs, and require human approval that shows the full parameters.
MCP makes a profound trade: it shifts control from developers to the LLM. The model, not a hand-written program, decides which tools to call and with what arguments. The correct mindset is to treat every tool description and every return value as a potential injection vector, enforce integrity checks, and require human oversight for sensitive actions. The most cited issues are over-privileged access, indirect prompt injection, tool poisoning and rug pulls, credential sprawl, and audit blind spots.
Authentication and authorization
Authentication splits cleanly along the transport line. stdio has no transport-layer authentication—no network, no header—so it draws credentials from the env block; security reduces to what you launch and what you grant it. Streamable HTTP can and should authenticate: it carries an Authorization header, and the recommended mechanism is OAuth for delegated, scoped access, with a gateway able to validate the credential before it reaches the server. Best practice is scoped, per-server, short-lived tokens (never one shared token), requesting the narrowest scope that works—mail.readonly rather than mail.full_access. Two failure modes matter: the confused deputy problem (a server executing with its own broad privileges rather than the user's restricted ones) and credential sprawl (shared tokens and over-scoped permissions across ungoverned servers creating aggregation risk).
Sandboxing untrusted servers
If you cannot fully trust a server—and with third-party servers you generally cannot—you contain it: run local servers in containers with restricted filesystem and network access; prefer stdio for local connections to limit network exposure; for HTTP/SSE servers, bind to 127.0.0.1, never 0.0.0.0, and validate the Origin header on every connection; and separate sensitive servers (payments, auth) from general-purpose tools. The Origin and localhost-binding rules are not folklore—the spec mandates them for Streamable HTTP to prevent DNS rebinding attacks, where a malicious website tricks a browser into interacting with a local MCP server the user never intended to expose.
Tool poisoning and supply-chain risk
The most distinctive MCP threat is tool poisoning, hinging on a fundamental asymmetry: the model sees the complete tool description and metadata, while the user sees only a simplified UI representation. Malicious instructions embedded in a tool's description or parameters are invisible to users but fully visible to the model—and an agent does not even need to use a poisoned tool to be affected; it only needs to read its description. "One drop of poison can infect every session where that tool is interacted with." The documented Invariant Labs / Cursor example: an innocent-looking add tool concealed directives to read ~/.cursor/mcp.json (other servers' credentials) and ~/.ssh/id_rsa and smuggle them out through function parameters, while the user saw only a simplified confirmation dialog.
flowchart TD
A["Attacker publishes poisoned tool (innocent-looking 'add' tool)"] --> B["Tool description hides malicious instructions"]
B --> C["Host loads server; model reads full description"]
C --> D["User sees only simplified confirmation dialog"]
D --> E["Model follows hidden directives"]
E --> F["Reads ~/.cursor/mcp.json and ~/.ssh/id_rsa"]
F --> G["Smuggles secrets out via function parameters"]
G --> H{"Credentials & SSH key exfiltrated"}
Three related patterns round out the family: tool shadowing (a malicious server injects instructions that alter the behavior of other, trusted tools—the "combined attack surface" hazard); rug pull (a tool is legitimate when approved, then the server silently changes its definition weeks later); and indirect (data) prompt injection (instructions hidden inside externally sourced data the agent reads through a server). The defenses layer up: tool integrity (pin and hash definitions, verify before execution, re-prompt on change, maintain an allowlist, run scanners like mcp-scan); least privilege and credential hygiene (scoped, per-server, short-lived credentials); input/output validation (treat all LLM-generated parameters as untrusted, strict JSON Schema, and validate tool outputs too since one tool's output can become another's input); human-in-the-loop approval that displays the full tool-call parameters, not a summary; and supply-chain and monitoring (trusted sources, checksums, typosquatting vigilance, centralized audit logs to a SIEM). Treat the server manifest as a signed, versioned, verifiable artifact—because it is precisely what poisoning and rug-pull attacks corrupt.
Animation slot: A tool card flips—the user-facing side shows a clean "add(a, b)" label, while the model-facing side reveals hidden red instruction text reaching toward ~/.ssh/id_rsa; a "pin & hash" padlock then clamps the card and a mismatch alert fires when the definition silently changes.
Post-Reading Check — Security and Trust
12. What is the recommended authentication mechanism for a remote (Streamable HTTP) server?
13. The MCP spec mandates binding local HTTP servers to 127.0.0.1 and validating the Origin header specifically to prevent which attack?
14. What fundamental asymmetry makes tool poisoning possible?
15. Which human-in-the-loop practice directly counters what tool poisoning weaponizes?