Skip to main content

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)

Already Know HTTP?

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:

  1. Client sends a request — "I want something from you"
  2. 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

MethodPurposeExample
GETRetrieve dataLoad a webpage, fetch user profile
POSTSend data to create/processSubmit a form, call an API
PUTUpdate existing dataEdit a user's settings
DELETERemove dataDelete 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:

CodeMeaningWhat It Tells You
200OKRequest succeeded
201CreatedResource was created
400Bad RequestYour request was malformed
401UnauthorizedAuthentication required/failed
404Not FoundResource doesn't exist
500Server ErrorSomething 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:

  1. Client sends HTTP POST request with JSON-RPC message
  2. Server processes the request
  3. Server responds with HTTP response body containing JSON-RPC result

Stateless vs Stateful Modes

Streamable HTTP supports two operational modes:

ModeResponse TypeSessionBest For
StatelessJSON or streamingNoneCloud scaling (multiple nodes), serverless, no MCP advanced features needed
StatefulSSE streamMaintainedLong-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:

CriterionstdioStreamable HTTP (Stateless)Streamable HTTP (Stateful)
DeploymentLocal onlyLocal or remoteLocal or remote
Client countSingleMultiple simultaneousMultiple (with session mgmt)
Launch mechanismSubprocess (automatic)External serviceExternal service
Latencyunder 1ms (local IPC)10-100ms (network)10-100ms + stream overhead
Setup complexitySimple JSON configService + networkingService + sessions + SSE
Suitable forDevelopment, desktopProduction, serverlessLong-running operations
AuthenticationNone (trusted localhost)Required (HTTP auth)Required (HTTP auth)
ScalabilityLinear (one per client)Horizontal (stateless)Requires sticky sessions
State managementIn-processNoneServer-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.