The Quick Answer
Choose stdio for local CLI tools, StreamableHTTP for web/remote access. SSE is deprecated—use StreamableHTTP instead. Here's the decision matrix:
| Transport | Use Case | Latency | Remote Access | Multi-Client | |-----------|----------|---------|---------------|--------------| | stdio | CLI/Local | Lowest | No | No | | StreamableHTTP | Web/API | Medium | Yes | Yes | | SSE (deprecated) | Legacy | High | Yes | Yes |
# Local CLI: stdio transport (default)$mcp-server-example# Remote/Web: StreamableHTTP endpoint$curl -X POST https://api.example.com/mcp \$ -H "Content-Type: application/json" \$ -d '{"jsonrpc":"2.0","method":"tools/list","id":1}'
This choice impacts performance, scalability, and deployment architecture. Stdio excels for local tools, while StreamableHTTP enables cloud deployments and browser integrations.
Transport Protocol Overview
MCP supports multiple transport protocols to accommodate different deployment scenarios. Each transport handles the conversion between MCP protocol messages and JSON-RPC 2.0 format, but they differ in how messages flow between client and server.
The transport layer sits between your MCP implementation and the network, managing connection lifecycle, message serialization, and error handling. Understanding these differences helps you choose the right transport for your use case.
stdio Transport
The stdio transport communicates through standard input/output streams, making it ideal for local integrations and command-line tools. It's the simplest and most performant option for single-client scenarios.
Implementation
// TypeScript stdio server
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
const server = new Server({
name: "example-server",
version: "1.0.0"
}, {
capabilities: {
tools: {},
resources: {}
}
});
// Connect via stdio transport
const transport = new StdioServerTransport();
await server.connect(transport);
# Python stdio server
from mcp.server import Server
from mcp.server.stdio import stdio_server
server = Server("example-server", version="1.0.0")
@server.tool()
async def example_tool(param: str) -> str:
"""Example tool implementation"""
return f"Processed: {param}"
# Run with stdio transport
if __name__ == "__main__":
stdio_server(server).run()
The stdio transport requires no network configuration—the client spawns the server process and communicates through pipes. This eliminates network overhead and security concerns but limits deployment to local environments.
Configuration Example
{
"mcpServers": {
"example": {
"command": "node",
"args": ["path/to/server.js"],
"env": {
"API_KEY": "your-key"
}
}
}
}
Use Cases
Stdio transport excels in scenarios requiring minimal latency and simple deployment:
- Command-line tools that need MCP capabilities
- Local development and testing environments
- Desktop applications with embedded MCP servers
- Scripts and automation tools
SSE Transport (Deprecated)
Server-Sent Events (SSE) was the original transport for remote MCP access but has been deprecated in favor of StreamableHTTP. While still supported for backward compatibility, new implementations should use StreamableHTTP.
SSE required maintaining two separate endpoints—one for client requests (HTTP POST) and another for server responses (SSE stream). This complexity led to its deprecation:
// Legacy SSE implementation (avoid for new projects)
app.post('/messages', async (req, res) => {
// Handle client messages
});
app.get('/sse', async (req, res) => {
res.setHeader('Content-Type', 'text/event-stream');
// Send server events
});
The dual-endpoint architecture complicated session management, error handling, and deployment. StreamableHTTP addresses these issues with a unified approach.
StreamableHTTP Transport
StreamableHTTP modernizes remote MCP access with a single-endpoint architecture supporting both request-response and streaming patterns. It's the recommended transport for web applications and distributed systems.
Implementation
// TypeScript StreamableHTTP server
import express from 'express';
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
const app = express();
const server = new Server({ name: "example", version: "1.0.0" });
app.post('/mcp', async (req, res) => {
const response = await server.handleRequest(req.body);
// Single response
if (!requiresStreaming(response)) {
return res.json(response);
}
// SSE stream for multiple messages
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
for await (const message of response.stream) {
res.write(`data: ${JSON.stringify(message)}\n\n`);
}
});
app.listen(3000);
# Python StreamableHTTP server with FastAPI
from fastapi import FastAPI, Request, Response
from fastapi.responses import StreamingResponse
from mcp.server import Server
import json
app = FastAPI()
server = Server("example", version="1.0.0")
@app.post("/mcp")
async def handle_mcp(request: Request):
body = await request.json()
response = await server.handle_request(body)
if not response.requires_streaming:
return response.to_json()
async def generate():
async for message in response.stream():
yield f"data: {json.dumps(message)}\n\n"
return StreamingResponse(
generate(),
media_type="text/event-stream"
)
StreamableHTTP's flexibility allows servers to choose response patterns based on the operation. Simple tool calls return JSON, while long-running operations or notifications use SSE streaming.
Session Management
// Client includes session ID for stateful connections
const response = await fetch('https://api.example.com/mcp', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json, text/event-stream',
'Mcp-Session-Id': sessionId // Optional session tracking
},
body: JSON.stringify(request)
});
Sessions enable advanced features like request deduplication, ordered message delivery, and connection recovery. The server can assign session IDs during initialization and track client state across requests.
Performance Comparison
Transport performance varies significantly based on deployment scenario. Here's a detailed comparison:
Latency Characteristics
# Measure stdio latency (typically <1ms)$time echo '{"jsonrpc":"2.0","method":"ping","id":1}' | mcp-server-stdio# Measure HTTP latency (typically 10-50ms)$time curl -X POST http://localhost:3000/mcp \$ -d '{"jsonrpc":"2.0","method":"ping","id":1}'
Stdio eliminates network stack overhead, providing microsecond-level response times. StreamableHTTP adds HTTP parsing and network transmission but enables remote access and horizontal scaling.
Resource Usage
| Metric | stdio | StreamableHTTP | |--------|-------|----------------| | Memory per connection | ~10MB | ~50MB | | CPU overhead | Minimal | HTTP parsing | | Concurrent clients | 1 | Unlimited | | Network bandwidth | 0 | Variable |
Throughput Testing
# Benchmark script for comparing transports
import asyncio
import time
from mcp.client import Client
from mcp.client.stdio import StdioTransport
from mcp.client.http import HttpTransport
async def benchmark_transport(transport, iterations=1000):
client = Client()
await client.connect(transport)
start = time.time()
for _ in range(iterations):
await client.call_tool("echo", {"message": "test"})
elapsed = time.time() - start
print(f"Operations/sec: {iterations / elapsed:.2f}")
await client.close()
# Test both transports
await benchmark_transport(StdioTransport("mcp-server"))
await benchmark_transport(HttpTransport("http://localhost:3000/mcp"))
In practice, stdio achieves 10,000+ operations/second while HTTP typically handles 100-1,000 ops/sec depending on network conditions and server implementation.
Common Issues
Understanding common transport-related errors helps diagnose integration problems quickly. Here are the most frequent issues and their solutions.
Transport Mismatch Error
Error: "SSE connection not established"
This occurs when clients expect SSE but servers use stdio. MCP Inspector, for example, always attempts SSE negotiation regardless of server configuration.
# Solution 1: Use stdio-to-SSE proxy$npm install -g @modelcontextprotocol/mcp-proxy$mcp-proxy --stdio-server "python server.py" --port 3000# Solution 2: Configure Inspector for stdio$export MCP_TRANSPORT=stdio$mcp-inspector --command "python server.py"
The proxy approach maintains compatibility with SSE-only clients while keeping your server implementation simple. Direct stdio configuration is faster but requires client support.
Connection Timeout Issues
Error: "Client timeout after 60000ms"
Network transports can timeout due to slow responses or connection problems. Stdio transports rarely timeout unless the server process hangs.
// Adjust timeout in client configuration
const client = new Client({
transport: new HttpTransport(url, {
timeout: 120000, // 2 minutes
retryAttempts: 3,
retryDelay: 1000
})
});
# Python timeout configuration
from mcp.client.http import HttpTransport
transport = HttpTransport(
url="http://localhost:3000/mcp",
timeout=120.0, # seconds
max_retries=3
)
Consider implementing health checks for production deployments. Long-running operations should provide progress updates to prevent timeouts.
CORS and Security Errors
Error: "CORS policy blocked request"
StreamableHTTP servers must handle CORS for browser-based clients. Stdio doesn't face this issue since it runs locally.
// Express CORS configuration
app.use((req, res, next) => {
res.header('Access-Control-Allow-Origin', 'https://app.example.com');
res.header('Access-Control-Allow-Methods', 'POST, GET, OPTIONS');
res.header('Access-Control-Allow-Headers', 'Content-Type, Mcp-Session-Id');
if (req.method === 'OPTIONS') {
return res.sendStatus(204);
}
next();
});
Always validate origins in production and use HTTPS for StreamableHTTP deployments. Authentication should happen at the transport layer, not within MCP messages.
Examples
Let's build a practical example showing all three transports serving the same MCP tools. This demonstrates how transport choice affects implementation complexity.
Multi-Transport Weather Server
// server.ts - MCP server supporting all transports
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import express from 'express';
// Create MCP server
const server = new Server({
name: "weather-server",
version: "1.0.0"
}, {
capabilities: {
tools: {}
}
});
// Register weather tool
server.setRequestHandler('tools/call', async (request) => {
if (request.params.name === 'get_weather') {
const { location } = request.params.arguments;
// Simulate API call
const weather = await fetchWeather(location);
return {
content: [{
type: "text",
text: `Weather in ${location}: ${weather.temp}°F, ${weather.condition}`
}]
};
}
});
// Transport option 1: stdio (for CLI usage)
if (process.argv.includes('--stdio')) {
const transport = new StdioServerTransport();
await server.connect(transport);
}
// Transport option 2: StreamableHTTP (for web usage)
if (process.argv.includes('--http')) {
const app = express();
app.use(express.json());
app.post('/mcp', async (req, res) => {
const response = await server.handleRequest(req.body);
res.json(response);
});
app.listen(3000, () => {
console.log('StreamableHTTP server running on port 3000');
});
}
Client Configuration Examples
{
"mcpServers": {
"weather-local": {
"command": "node",
"args": ["weather-server.js", "--stdio"]
},
"weather-remote": {
"transport": "http",
"url": "https://api.weather.example.com/mcp",
"headers": {
"Authorization": "Bearer token123"
}
}
}
}
Production Deployment Pattern
# deploy.py - Production server with monitoring
from fastapi import FastAPI, Request, HTTPException
from prometheus_client import Counter, Histogram, generate_latest
import logging
from mcp.server import Server
# Metrics
request_count = Counter('mcp_requests_total', 'Total MCP requests')
request_duration = Histogram('mcp_request_duration_seconds', 'Request duration')
app = FastAPI()
mcp_server = Server("weather-server", version="1.0.0")
@app.post("/mcp")
@request_duration.time()
async def handle_mcp(request: Request):
request_count.inc()
try:
body = await request.json()
# Validate session if provided
session_id = request.headers.get('Mcp-Session-Id')
if session_id and not validate_session(session_id):
raise HTTPException(status_code=401, detail="Invalid session")
response = await mcp_server.handle_request(body)
return response
except Exception as e:
logging.error(f"MCP request failed: {e}")
raise HTTPException(status_code=500, detail="Internal server error")
@app.get("/metrics")
async def metrics():
return Response(generate_latest(), media_type="text/plain")
# Health check endpoint
@app.get("/health")
async def health():
return {"status": "healthy", "transport": "streamablehttp"}
This production setup includes monitoring, error handling, and health checks—essential for reliable StreamableHTTP deployments but unnecessary for stdio servers.
Migration Guide
Moving from SSE to StreamableHTTP requires updating both server and client code. Here's a step-by-step migration approach.
Server Migration
// Before: SSE with dual endpoints
app.post('/messages', handleMessages);
app.get('/sse', handleSSE);
// After: StreamableHTTP with single endpoint
app.post('/mcp', async (req, res) => {
const response = await server.handleRequest(req.body);
if (response.streaming) {
res.setHeader('Content-Type', 'text/event-stream');
streamResponse(res, response);
} else {
res.json(response);
}
});
Client Migration
# Before: SSE client with two connections
sse_client = SSEClient(
message_endpoint="http://api.example.com/messages",
sse_endpoint="http://api.example.com/sse"
)
# After: StreamableHTTP client with single endpoint
http_client = HttpClient(
endpoint="http://api.example.com/mcp",
supports_streaming=True
)
Backward Compatibility
Support both transports during migration to avoid breaking existing clients:
// Dual-transport server
app.post('/mcp', handleStreamableHTTP); // New endpoint
app.post('/messages', handleLegacyMessages); // Legacy SSE
app.get('/sse', handleLegacySSE); // Legacy SSE
// Remove legacy endpoints after client migration
Monitor usage metrics to track migration progress and safely deprecate SSE endpoints once traffic drops to zero.
Related Guides
Building a stdio MCP server
Build MCP servers with stdio transport for local CLI tools and subprocess communication.
Building a StreamableHTTP MCP server
Deploy scalable MCP servers using StreamableHTTP for cloud environments and remote access.
Configuring MCP transport protocols for Docker containers
Configure MCP servers in Docker containers with proper transport protocols and networking.