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.

MERMAID
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:

TYPESCRIPT
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