Transport Layers: How MCP Messages Travel
You've configured an MCP server on your laptop. It works perfectly—Claude Code can read files, query databases, create issues. But what happens when you want to deploy that server to the cloud so your whole team can use it? Or run it on a customer's infrastructure?
The good news: your MCP server code doesn't change. Only the transport—how messages physically travel between client and server—changes.
Think of it like phone calls. Whether you call someone via landline, cellular, or WiFi calling, the conversation is the same. Only the underlying network changes. MCP works the same way: the protocol (JSON-RPC messages) stays identical whether messages travel through local process streams or across the internet via HTTP.
This lesson explores that separation. You'll understand when to use stdio transport (perfect for development and single-client scenarios), when to use Streamable HTTP transport (necessary for remote servers and multi-client products), and most importantly, why your server code doesn't need to know the difference.
Understanding Transport Agnosticism
Before diving into specific transports, let's establish why this architecture choice matters.
Traditional approach: Lock tools to a single communication mechanism. GraphQL typically uses HTTP. gRPC requires its own protocol. WebSocket connections have different semantics than REST APIs.
MCP's approach: Define the protocol (JSON-RPC message structure, capability discovery, tool schemas) completely separately from the transport. A tool definition works identically whether it arrives via subprocess streams or HTTP requests.
This separation means:
- Developers focus on business logic (what tools do, what resources expose) without transport concerns
- Deployers choose optimal transport based on infrastructure (local for testing, remote for production)
- Systems scale horizontally with zero application changes—just switch to HTTP transport
You'll see the same pattern across professional systems: email protocol (SMTP, POP, IMAP) works over different underlying transports; web browsers speak HTTP whether connected via broadband or wireless; APIs remain unchanged whether accessed from local network or cloud.
MCP's transport independence is not convenient—it's architectural maturity.
stdio Transport: Local Communication via Streams
How It Works
When you add an MCP server to Claude Code's configuration, Claude launches that server as a subprocess. The two programs then communicate through standard input/output streams:
- Client writes JSON-RPC requests to the server's stdin
- Server reads from stdin, processes, and writes responses to stdout
- Client reads responses from the server's stdout
This is the simplest possible IPC (inter-process communication) mechanism. No network layer. No HTTP headers. No authentication negotiation. Just process streams.
┌─────────────────────────────────────────┐
│ MCP Host (Client) │
│ │
│ Launches: mcp-server │
│ │ │
│ ←─────────────→ stdin/stdout │
│ │
└─────────────────────────────────────────┘
▲
│
│ subprocess streams
│
▼
┌─────────────────────────────────────────┐
│ MCP Server (subprocess) │
│ │
│ Reads from stdin │
│ Processes tool calls │
│ Writes to stdout │
│ │
└─────────────────────────────────────────┘
Critical Detail: Log Redirection
Here's where many developers stumble: stdio has ONE output stream. Both regular output and error messages use stdout. This creates a problem—how does the client distinguish message protocol from debug logs?
MCP solves this with strict stream discipline:
- Messages go to stdout (JSON-RPC request/response protocol)
- Logs go to stderr (diagnostic information, warnings, errors)
This means if your MCP server implementation prints to stdout instead of stderr, you corrupt the protocol stream. The client tries to parse "Starting server..." as JSON-RPC and fails.
This is not theoretical—it's a common debugging scenario:
# WRONG - breaks stdio transport
print("Server starting") # Goes to stdout, corrupts protocol
result = tool_function()
# CORRECT - preserves protocol stream
import sys
print("Server starting", file=sys.stderr) # Goes to stderr
result = tool_function()
When to Use stdio
Best for:
- Local development: Server and client on same machine
- Desktop applications: Claude Code, Cursor, Zed
- Single-client scenarios: One user, one instance of the server
- Simplicity: Minimal configuration, no networking
Limitations:
- Not remote: Can't reach server on different machine
- Not multi-client: Each client needs its own subprocess
- Process dependent: Server must be executable in client's environment
HTTP Fundamentals (Optional Background)
If you've built web APIs, used fetch() or requests, or understand how browsers load websites, skip to Streamable HTTP Transport below. This section provides foundation for those new to web protocols.
When you type a URL into your browser, you're using HTTP (HyperText Transfer Protocol)—the communication standard that powers the web. Understanding HTTP basics is essential for MCP's remote transport.
The Request-Response Model
HTTP follows a simple pattern:
- Client sends a request — "I want something from you"
- Server sends a response — "Here's what you asked for" (or an error)
Every HTTP interaction is one request producing one response. The client always initiates; the server always responds.
HTTP Methods: What You Want to Do
| Method | Purpose | Example |
|---|---|---|
| GET | Retrieve data | Load a webpage, fetch user profile |
| POST | Send data to create/process | Submit a form, call an API |
| PUT | Update existing data | Edit a user's settings |
| DELETE | Remove data | Delete a comment |
MCP uses POST exclusively—every MCP message is sent via POST request because you're sending JSON-RPC data for the server to process.
Headers: Metadata About Your Request
Headers are key-value pairs that travel with requests and responses. Think of them as the envelope around your letter—they describe the contents without being the contents.
Common headers you'll encounter:
Content-Type: application/json— "My request body is JSON"Authorization: Bearer abc123— "Here's my access token"Accept: text/event-stream— "I can receive streaming data"
Status Codes: Did It Work?
Servers respond with a three-digit code indicating what happened:
| Code | Meaning | What It Tells You |
|---|---|---|
| 200 | OK | Request succeeded |
| 201 | Created | Resource was created |
| 400 | Bad Request | Your request was malformed |
| 401 | Unauthorized | Authentication required/failed |
| 404 | Not Found | Resource doesn't exist |
| 500 | Server Error | Something broke on the server |
When MCP uses HTTP transport, you'll see these codes in logs and error messages.
Server-Sent Events (SSE): One-Way Streaming
Standard HTTP is request-response: one request, one response, done. But what if the server needs to send multiple messages over time—like progress updates during a long operation?
Server-Sent Events (SSE) solves this. The client makes one request, and the server keeps the connection open, streaming multiple messages:
Client: POST /mcp (start operation)
Server: data: {"progress": 10}
Server: data: {"progress": 50}
Server: data: {"progress": 100}
Server: data: {"result": "done"}
SSE is one-way (server to client only) and works over standard HTTP, no special protocols needed. MCP's stateful HTTP transport uses SSE for streaming responses.
With these fundamentals in place, let's see how MCP applies them.
Streamable HTTP Transport: Remote Communication
How It Works
When an MCP server runs on a remote machine (or needs to serve multiple clients), the architecture shifts. The server becomes a persistent service listening on an HTTP endpoint. Clients connect via HTTP POST requests:
- Client sends HTTP POST request with JSON-RPC message
- Server processes the request
- Server responds with HTTP response body containing JSON-RPC result
Stateless vs Stateful Modes
Streamable HTTP supports two operational modes:
| Mode | Response Type | Session | Best For |
|---|---|---|---|
| Stateless | JSON or streaming | None | Cloud scaling (multiple nodes), serverless, no MCP advanced features needed |
| Stateful | SSE stream | Maintained | Long-running operations, progress updates, complex workflows |
Stateless mode (recommended for cloud deployments):
- Client sends HTTP POST with JSON-RPC request
- Server can return single JSON OR streaming response
- No session state between requests
- Essential for horizontal scaling (load balancers, multi-node, serverless)
- Use when you don't need MCP advanced features (sampling, roots, etc.)
Client → POST /mcp {"method": "tools/call", ...}
Server → 200 OK {"result": {...}} (single response)
Stateful mode (for streaming scenarios):
- Server maintains session across requests
- Uses Server-Sent Events (SSE) for streaming responses
- Required for long-running tool executions with progress updates
- More complex to deploy (sticky sessions, connection management)
Client → POST /mcp {"method": "tools/call", ...}
Server → 200 OK (SSE stream)
data: {"progress": 25}
data: {"progress": 50}
data: {"result": {...}}
For most use cases, stateless JSON is simpler and scales better—only use stateful SSE when you genuinely need streaming progress or real-time updates.
Multi-Client Architecture
Unlike stdio (one client per server process), Streamable HTTP allows multiple clients to connect to a single server:
┌──────────────────────────────────────┐
│ Client A │
│ │
│ HTTP POST /mcp │
│ {tool request...} │
│ ↓ │
│ [JSON or SSE response] │
└──────────────────────────────────────┘
┌──────────────────────────────────────┐
│ Client B │
│ │
│ HTTP POST /mcp │
│ {tool request...} │
│ ↓ │
│ [JSON or SSE response] │
└──────────────────────────────────────┘
▲ ▲
│ │
HTTP requests (multiple clients)
│ │
▼ ▼
┌──────────────────────────────────────┐
│ MCP Server (remote service) │
│ │
│ Listens on :8000 │
│ Handles multiple clients │
│ Returns JSON or streams SSE │
│ │
└──────────────────────────────────────┘
Authentication
HTTP transport requires securing the connection. MCP supports standard HTTP authentication mechanisms:
- Bearer tokens: Client includes
Authorization: Bearer <token>header - OAuth 2.1: Full authentication flow with token exchange
- Custom headers: Implementation-specific authentication
The server validates credentials before processing requests. This isn't extra security layering—it's integral to HTTP transport, unlike stdio which assumes trusted localhost communication.
When to Use Streamable HTTP
Best for:
- Remote servers: Server on different machine/cloud service
- Multi-client products: Multiple users/clients accessing same server
- Cloud deployment: Serverless functions, managed services
- Standard infrastructure: Works with any HTTP client library
Complexity:
- Requires running server as persistent service (not subprocess)
- Network configuration (firewall, routing)
- Authentication setup
- Monitoring and health checks
Transport Comparison: When to Choose Which
Here's how the transports compare across practical dimensions:
| Criterion | stdio | Streamable HTTP (Stateless) | Streamable HTTP (Stateful) |
|---|---|---|---|
| Deployment | Local only | Local or remote | Local or remote |
| Client count | Single | Multiple simultaneous | Multiple (with session mgmt) |
| Launch mechanism | Subprocess (automatic) | External service | External service |
| Latency | under 1ms (local IPC) | 10-100ms (network) | 10-100ms + stream overhead |
| Setup complexity | Simple JSON config | Service + networking | Service + sessions + SSE |
| Suitable for | Development, desktop | Production, serverless | Long-running operations |
| Authentication | None (trusted localhost) | Required (HTTP auth) | Required (HTTP auth) |
| Scalability | Linear (one per client) | Horizontal (stateless) | Requires sticky sessions |
| State management | In-process | None | Server-side sessions |
Decision Framework
Choose stdio when:
- Building features for Claude Code, Cursor, or similar local IDEs
- Prototyping and testing MCP servers
- Your MCP server runs on developer's machine
- Working in single-client context
Choose Streamable HTTP (Stateless) when:
- Server runs on cloud infrastructure or different machine
- Multiple users/applications need to access same MCP server
- Building production MCP services with serverless (Lambda, Cloud Run)
- Server needs to scale horizontally with load balancers
- Tools complete quickly (< few seconds)
Choose Streamable HTTP (Stateful) when:
- Tools take significant time (code generation, large file processing)
- Users need progress updates during execution
- Workflow involves multiple back-and-forth exchanges
- Building interactive experiences requiring real-time feedback
The Data Layer Remains Transport-Independent
This is the crucial insight: your JSON-RPC message structure doesn't change.
Whether a tool request travels through stdio or HTTP, the actual message is identical:
{
"jsonrpc": "2.0",
"id": "123",
"method": "tools/call",
"params": {
"name": "search_codebase",
"arguments": {
"pattern": "async"
}
}
}
The transport layer handles how this message gets from client to server. Your tool implementation, parameter validation, and business logic remain unchanged.
This is why experienced developers care about this architecture: it means switching a server from local development (stdio) to production deployment (HTTP) requires updating configuration, not rewriting code.
An upcoming lesson covers how to configure MCP clients in various hosts—you'll see how transport selection is purely a configuration concern.
Try With AI
Use your AI companion to explore transport layer concepts and decision-making.
Prompt 1: Discover Your Current Transport (No Setup Required)
I use Claude Code with several MCP servers configured. Help me understand
which transport each one uses. Here's my config (show your .claude/config.json
or relevant configuration snippet):
[paste your MCP configuration]
For each server, explain:
1. Is it stdio or HTTP based on the configuration?
2. How do you know from the config?
3. What would need to change if we moved it from local to cloud?
What you're learning: Recognizing transport choice from configuration structure; understanding what configuration reveals about deployment architecture.
Prompt 2: Diagnose a Transport Problem
My MCP server works fine locally in Claude Code, but when we try to
run it on our remote server, clients can't connect. The server starts
without errors but responds with blank responses.
The server uses stdio transport locally. I moved it to HTTP by:
1. Changed from subprocess to Flask service
2. Removed stdin/stdout communication
3. Added HTTP endpoints
What's probably wrong? What questions would you ask to diagnose this?
What you're learning: Connecting transport configuration to communication flow; identifying common mistakes when switching transports.
Prompt 3: Evaluate Transport for a Scenario
We're building an internal tool that helps engineers refactor Python code
using AI. The tool needs to:
- Run AI-powered code analysis
- Access files on the engineer's machine
- Provide results back in the editor
- Eventually: serve 50+ engineers simultaneously from cloud
Should we use stdio or HTTP transport initially? What about after launch?
What changes when we scale from 1 user to 50+?
Walk me through the trade-offs and explain your recommendation.
What you're learning: Making deployment architecture decisions based on requirements; understanding how scalability drives transport choices.
Safety Note on Transport Configuration
When setting up HTTP transport in production, always verify authentication is enabled—never expose MCP endpoints without requiring bearer tokens or OAuth. Local stdio transport is inherently safer because it requires the server to be running as a subprocess on your machine, but HTTP services are network-reachable and need explicit access control.