Streaming JSON-RPC over HTTP: The Evolution of MCP Transports
The Model Context Protocol specification initially supported two primary transports: local standard input/output (stdio) for command-line tools and desktop clients, and Server-Sent Events paired with HTTP POST (sse + /messages) for networked services.
While functional, legacy SSE suffered from operational friction in distributed cloud topologies: dual-endpoint session handshake complexity, middlebox proxy timeouts, and disjointed request-response correlation. The modern Streamable HTTP Transport (MCP 2025-03-26+ Spec) solves these challenges by unifying bidirectional JSON-RPC streaming into a single HTTP route.
sequenceDiagram
participant Client as MCP Client (Cursor / Claude / Agent)
participant Edge as Edge Proxy (Cloudflare / Envoy)
participant Server as MCP Streamable Server (/mcp)
Client->>Server: POST /mcp (initialize request)
Note over Client,Server: Accept: application/json, text/event-stream
Server->>Client: 200 OK (mcp-session-id: uuid-8821a)
Client->>Server: POST /mcp (callTool: search_articles)
Note over Client,Server: Headers: mcp-session-id: uuid-8821a
Server-->>Client: Streamed chunk 1: Tool execution started...
Server-->>Client: Streamed chunk 2: JSON results payload
Server->>Client: 200 Finished (Keep-Alive)
1. Comparing Legacy SSE vs Streamable HTTP
| Metric / Capability | Legacy SSE (/sse + /messages) |
Modern Streamable HTTP (/mcp) |
|---|---|---|
| Endpoint Topology | Two endpoints (/sse for down, /messages for up) |
Single unified endpoint (/mcp) |
| HTTP Methods | GET for event stream, POST for messages |
Standard POST, GET, DELETE |
| Session Identification | URL query parameter (?sessionId=...) |
Standard Header (mcp-session-id: ...) |
| Streaming Support | Unidirectional server-to-client push | Bidirectional chunked transfer streaming |
| Proxy Compatibility | Prone to buffering in CDNs | Fully compatible with standard HTTP/2 and HTTP/3 |
2. Implementing the Streamable Transport in TypeScript
Using @modelcontextprotocol/sdk/server/streamableHttp.js, an Express server mounts the transport with session tracking:
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
import { randomUUID } from 'node:crypto';
const streamableTransports = new Map<string, StreamableHTTPServerTransport>();
app.all('/mcp', async (req, res) => {
const sessionId = req.headers['mcp-session-id'] as string | undefined;
let transport = sessionId ? streamableTransports.get(sessionId) : undefined;
if (!transport) {
transport = new StreamableHTTPServerTransport({
sessionIdGenerator: () => randomUUID(),
onsessioninitialized: (id) => {
streamableTransports.set(id, transport!);
},
onsessionclosed: (id) => {
streamableTransports.delete(id);
}
});
await mcpServer.connect(transport);
}
await transport.handleRequest(req, res, req.body);
});
3. Operational Best Practices
- Always Set
mcp-session-idon Outgoing Requests: In stateless multi-instance clusters, configure ingress controllers (e.g. Traefik, HAProxy, Cloudflare Workers) to pin sticky sessions based on themcp-session-idheader. - Graceful Session Invalidation: Clients should send an explicit
DELETE /mcprequest withmcp-session-idupon task completion to purge cached server resources. - Handle Edge Buffering: Configure reverse proxies to disable response buffering (
X-Accel-Buffering: noin NGINX) to enable instant tool progress streaming.