Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Chapter 7: Transports

How Bits Get From Here to There

MCP is a protocol, and protocols need a way to move messages between participants. The transport layer is the plumbing—it doesn’t care about tools, resources, or prompts. It only cares about getting JSON-RPC messages from the client to the server and back again, reliably and in order.

MCP defines two official transports: stdio for local communication and Streamable HTTP for remote communication. There’s also a legacy SSE (Server-Sent Events) transport that you might encounter in older implementations.

stdio: The Local Transport

The stdio transport is beautifully simple. The host spawns the MCP server as a child process and communicates through standard input (stdin) and standard output (stdout). That’s it. No networking, no ports, no TLS, no authentication. Just pipes.

How It Works

┌──────────────┐            ┌──────────────┐
│     Host     │            │  MCP Server  │
│              │───stdin───→│  (child      │
│              │←──stdout───│   process)   │
│              │   stderr→  │              │
└──────────────┘  (logging) └──────────────┘
  1. The host starts the server as a child process: npx @modelcontextprotocol/server-filesystem /home/user
  2. The host writes JSON-RPC messages to the server’s stdin
  3. The server writes JSON-RPC messages to its stdout
  4. stderr is reserved for logging and diagnostic output (it goes to the host’s log, not the protocol)

Message Framing

Each message is a single line of JSON followed by a newline character. No length prefixes, no delimiters—just newline-separated JSON:

{"jsonrpc":"2.0","id":1,"method":"initialize","params":{...}}\n
{"jsonrpc":"2.0","id":1,"result":{...}}\n
{"jsonrpc":"2.0","method":"notifications/initialized"}\n

This makes debugging trivial. You can literally read the server’s stdout to see what it’s saying. You can echo a JSON message into the server’s stdin to test it.

When to Use stdio

Use stdio when:

  • The server runs on the same machine as the host
  • You want zero-configuration networking
  • You need access to local resources (files, databases, processes)
  • You’re developing and want the simplest possible setup
  • You want the server to run with the user’s OS permissions

Configuration Example

In Claude Desktop’s claude_desktop_config.json:

{
  "mcpServers": {
    "filesystem": {
      "command": "npx",
      "args": ["-y", "@modelcontextprotocol/server-filesystem", "/home/user/projects"],
      "env": {
        "NODE_ENV": "production"
      }
    },
    "python-tools": {
      "command": "uvx",
      "args": ["my-mcp-server"],
      "env": {
        "API_KEY": "sk-..."
      }
    }
  }
}

The host spawns each server with the given command, arguments, and environment variables. When the host shuts down, it kills the child processes.

The stderr Convention

A critical detail: servers MUST NOT write protocol messages to stderr. stderr is for human-readable logs, debug output, and error traces. The host typically captures stderr and writes it to a log file.

If your server accidentally writes a log line to stdout, the client will try to parse it as JSON-RPC, fail, and probably disconnect. This is the #1 source of “my MCP server doesn’t work” bug reports.

# BAD - this goes to stdout and breaks the protocol
print("Server starting...")

# GOOD - this goes to stderr
import sys
print("Server starting...", file=sys.stderr)
// BAD
console.log("Server starting...");

// GOOD
console.error("Server starting...");

Streamable HTTP: The Remote Transport

Streamable HTTP is MCP’s transport for remote servers. Introduced in the 2025-03-26 spec revision to replace the older SSE transport, it’s designed for the real world of load balancers, CDNs, serverless functions, and corporate firewalls.

How It Works

The client communicates with the server over HTTP. The server exposes a single endpoint (by default /mcp) that accepts POST requests. The server can optionally support GET requests for server-to-client streaming.

┌──────────────┐              ┌──────────────┐
│    Client    │──HTTP POST──→│  MCP Server  │
│              │←─Response────│   (remote)   │
│              │              │              │
│              │──GET (SSE)──→│              │
│              │←─Events──────│              │
└──────────────┘              └──────────────┘

POST Requests (Client → Server)

The client sends JSON-RPC messages as HTTP POST requests:

POST /mcp HTTP/1.1
Content-Type: application/json
Accept: application/json, text/event-stream

{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}

The server can respond in two ways:

Direct JSON response (for simple request-response):

HTTP/1.1 200 OK
Content-Type: application/json

{"jsonrpc":"2.0","id":1,"result":{"tools":[...]}}

SSE stream (for responses that include progress or multiple messages):

HTTP/1.1 200 OK
Content-Type: text/event-stream

event: message
data: {"jsonrpc":"2.0","method":"notifications/progress","params":{"progressToken":"t1","progress":50,"total":100}}

event: message
data: {"jsonrpc":"2.0","id":1,"result":{"content":[{"type":"text","text":"Done!"}]}}

GET Requests (Server → Client Streaming)

For server-initiated messages (notifications, sampling requests, elicitations), the client can open a GET request that the server holds open as an SSE stream:

GET /mcp HTTP/1.1
Accept: text/event-stream

The server keeps this connection open and pushes events as needed:

HTTP/1.1 200 OK
Content-Type: text/event-stream

event: message
data: {"jsonrpc":"2.0","method":"notifications/tools/list_changed"}

event: message
data: {"jsonrpc":"2.0","id":"s1","method":"sampling/createMessage","params":{...}}

Session Management

Streamable HTTP supports optional session management via the Mcp-Session-Id header:

  1. During initialization, the server may return a session ID:
HTTP/1.1 200 OK
Mcp-Session-Id: abc123
Content-Type: application/json

{"jsonrpc":"2.0","id":1,"result":{...}}
  1. The client includes this header in subsequent requests:
POST /mcp HTTP/1.1
Mcp-Session-Id: abc123
Content-Type: application/json

{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}

Sessions are optional. Stateless servers can omit session management entirely.

When to Use Streamable HTTP

Use Streamable HTTP when:

  • The server is on a different machine (cloud, another office, another continent)
  • Multiple users share the same server
  • You need authentication and authorization
  • You want to deploy behind a load balancer or CDN
  • The server wraps a cloud API or SaaS service
  • You’re building a multi-tenant service

SSE (Legacy Transport)

The SSE transport was MCP’s original HTTP-based transport. It used two endpoints:

  1. GET /sse — Client opens an SSE connection for server-to-client messages
  2. POST /messages — Client sends messages to the server

You’ll still encounter SSE in:

  • Older MCP servers that haven’t been updated
  • Tutorials and blog posts from early 2025
  • Some client implementations that haven’t adopted Streamable HTTP yet

Most SDKs now support both, with Streamable HTTP as the default. If you’re building something new, use Streamable HTTP.

Transport Comparison

FeaturestdioStreamable HTTPSSE (Legacy)
DeploymentLocal onlyLocal or remoteLocal or remote
SetupZero configRequires HTTP serverRequires HTTP server
AuthenticationOS-levelHTTP auth (OAuth, tokens)HTTP auth
PerformanceFastest (no network)Network latencyNetwork latency
ScalabilitySingle userMulti-user, load balancedMulti-user
Firewall-friendlyN/A (local)Yes (standard HTTP)Mostly (SSE can be finicky)
BidirectionalYes (via pipes)Yes (POST + SSE stream)Yes (POST + SSE)
Session supportImplicit (process)Optional (header)Required (SSE endpoint)
Stateless supportNoYesNo

The Proxy Pattern

A common deployment pattern bridges local and remote transports using a proxy:

┌────────────┐     stdio    ┌───────────┐    HTTP    ┌────────────┐
│   Client   │─────────────→│   Proxy   │───────────→│   Remote   │
│            │←─────────────│           │←───────────│   Server   │
└────────────┘              └───────────┘            └────────────┘

The proxy runs locally, presents itself as a stdio server to the client, and forwards messages to a remote server over HTTP. This lets clients that only support stdio (like some older versions of Claude Desktop) connect to remote servers.

Tools like mcp-proxy implement this pattern. You configure your client to launch the proxy as a local command, and the proxy handles the HTTP communication.

Transport Security

stdio Security

stdio servers inherit the OS permissions of the user who launched them. There’s no additional authentication—if you can start the process, you can use it.

This is both a strength and a limitation. It’s simple and secure for local use (the server can only do what the user can do), but it means the server has full access to everything the user can access. A malicious MCP server running via stdio could read your files, environment variables, and credentials.

Bottom line: Only run stdio servers from trusted sources. Treat them like any other program you install.

Streamable HTTP Security

Remote servers need authentication. MCP supports:

  • OAuth 2.1 — The preferred method for production deployments. MCP defines a specific OAuth flow for client authentication (see Chapter 13).
  • API keys — Simpler but less secure. Typically passed as headers.
  • mTLS — Mutual TLS for high-security environments.

The transport layer handles TLS encryption (always use HTTPS for remote servers), but authentication and authorization are application concerns that live above the transport.

The Future of Transports

The MCP team is actively evolving the transport layer. Based on publicly discussed plans:

Stateless Architecture

The current Streamable HTTP transport supports stateful sessions. The future direction is toward a fully stateless protocol where each request is self-contained. This would:

  • Eliminate the need for sticky sessions
  • Enable serverless deployments (AWS Lambda, Cloudflare Workers)
  • Simplify horizontal scaling
  • Make load balancing trivial

Server Cards

A proposed /.well-known/mcp.json endpoint would let clients discover server capabilities before connecting. This metadata document would describe:

  • Available capabilities
  • Authentication requirements
  • Rate limits
  • Server version information

Think of it like robots.txt but for MCP.

Session Layer Changes

Sessions would move from the transport layer (implicit in the connection) to the data model layer (explicit, cookie-like). This decouples session state from connection state, enabling scenarios where a client reconnects to a different server instance and resumes its session.

Debugging Transports

When things go wrong, transport debugging is usually the first stop.

stdio Debugging

  1. Check stderr: Most servers log to stderr. Capture it and read it.
  2. Manual testing: You can pipe JSON directly to the server:
    echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-11-25","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}}}' | npx @modelcontextprotocol/server-filesystem /tmp
    
  3. Watch stdout: The server’s responses appear on stdout. If you see garbled output, something is writing non-JSON to stdout.

Streamable HTTP Debugging

  1. Use curl: Test endpoints directly:
    curl -X POST http://localhost:3000/mcp \
      -H "Content-Type: application/json" \
      -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{...}}'
    
  2. Check CORS: If the client is browser-based, CORS headers must be correct
  3. Check TLS: Certificate issues are a common source of connection failures
  4. Browser DevTools: For SSE connections, the Network tab shows the event stream

The MCP Inspector

The MCP Inspector (covered in Chapter 15) is the official debugging tool. It connects to any MCP server, sends requests, and shows responses in a nice UI. It’s the first thing to reach for when debugging transport issues.

Summary

MCP has two official transports: stdio for local servers (fast, simple, zero config) and Streamable HTTP for remote servers (scalable, authenticated, load-balanced). The legacy SSE transport is still supported but being phased out.

The transport layer is intentionally thin—it moves JSON-RPC messages and stays out of the way. This simplicity makes MCP easy to implement in any language and any environment, from a Raspberry Pi running a stdio server to a Kubernetes cluster hosting HTTP endpoints.

Now let’s build something. Time to write actual MCP servers.