MP-201a · Module 2

Streamable HTTP Servers

4 min read

Streamable HTTP is the MCP transport for remote servers. Instead of launching a child process, the client sends HTTP POST requests to your server's MCP endpoint. The server processes the JSON-RPC message and returns the response in the HTTP body. For long-running operations, the server can upgrade to Server-Sent Events (SSE) to stream progress updates before sending the final result. This transport enables centralized MCP servers that multiple clients share — a team-wide knowledge base, a shared database gateway, or a production API wrapper.

Session management is the key difference from stdio. A stdio server has one client and one session — the process is the session. An HTTP server handles multiple concurrent clients, each with their own session state. The MCP SDK provides session IDs via the Mcp-Session-Id header. Your server must route requests to the correct session and clean up state when sessions expire. For stateless tools (pure functions with no side effects), you can skip session tracking entirely.

import express from "express";
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StreamableHTTPServerTransport } from
  "@modelcontextprotocol/sdk/server/streamableHttp.js";
import {
  ListToolsRequestSchema,
  CallToolRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";

const app = express();
app.use(express.json());

// Store transports by session ID
const sessions = new Map<string, StreamableHTTPServerTransport>();

app.post("/mcp", async (req, res) => {
  const sessionId = req.headers["mcp-session-id"] as string | undefined;

  if (sessionId && sessions.has(sessionId)) {
    // Existing session — route to its transport
    const transport = sessions.get(sessionId)!;
    await transport.handleRequest(req, res);
    return;
  }

  // New session — create server + transport
  const server = new Server(
    { name: "remote-tools", version: "1.0.0" },
    { capabilities: { tools: {} } }
  );

  // Register tools (same pattern as stdio)
  server.setRequestHandler(ListToolsRequestSchema, async () => ({
    tools: [/* your tool definitions */],
  }));
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
    // your tool logic
    return { content: [{ type: "text", text: "result" }] };
  });

  const transport = new StreamableHTTPServerTransport({
    sessionIdGenerator: () => crypto.randomUUID(),
  });
  sessions.set(transport.sessionId!, transport);
  await server.connect(transport);
  await transport.handleRequest(req, res);
});

// Handle session cleanup
app.delete("/mcp", (req, res) => {
  const sessionId = req.headers["mcp-session-id"] as string;
  sessions.delete(sessionId);
  res.status(200).end();
});

app.listen(3001, () => console.log("MCP HTTP server on :3001"));

Do This

  • Use streamable HTTP when multiple clients need to share a server or when the server runs remotely
  • Implement session cleanup (DELETE handler or TTL expiry) to prevent memory leaks
  • Set CORS headers if any client might be browser-based
  • Return proper HTTP status codes — 400 for bad requests, 404 for unknown sessions, 500 for server errors

Avoid This

  • Use HTTP transport for local-only tools — stdio is simpler and has no network surface area
  • Store unbounded state per session without expiry — this is a memory leak in production
  • Forget authentication — an open HTTP MCP server is an open API anyone can call
  • Mix MCP and non-MCP routes on the same path — use a dedicated /mcp endpoint