If you have built an MCP server before, you probably used stdio transport for local development or SSE for remote deployments. Streamable HTTP is the transport the MCP specification now recommends for any server that runs over the network. It replaces SSE with a simpler model that works with standard HTTP infrastructure instead of fighting it.
This guide walks through building an MCP server with streamable HTTP transport from scratch. We will cover both Node.js and Python, handle streaming responses, and get the server deployed where clients can reach it.
Before you start
You need:
- Node.js 18+ or Python 3.10+
- The MCP SDK for your language (
@modelcontextprotocol/sdkfor Node.js,mcpfor Python) - A basic understanding of how MCP servers work (tools, resources, the JSON-RPC protocol)
If you have not built an MCP server before, start with our Node.js guide or Python guide first. This guide assumes you know how to define tools and handle requests.
Why streamable HTTP over SSE
Streamable HTTP uses a single HTTP endpoint. Clients POST JSON-RPC requests, servers respond with standard HTTP responses. No long-lived connections, no dual-endpoint setup, no persistent SSE streams that break behind load balancers.
When your server needs to stream — progress updates, partial results, notifications during a long tool call — it upgrades the response Content-Type to text/event-stream inline. The client handles both cases transparently. You get the simplicity of request-response for simple tools and the power of streaming when you actually need it.
For deployment, this is a significant win. Streamable HTTP works on serverless platforms, behind standard reverse proxies, and with normal HTTP load balancing. No special infrastructure.
Node.js implementation
Install the SDK:
npm install @modelcontextprotocol/sdk express
Create your server file:
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import express from "express";
const app = express();
app.use(express.json());
const server = new McpServer({
name: "my-server",
version: "1.0.0",
});
// Define a tool
server.tool("get_weather", { city: { type: "string" } }, async ({ city }) => {
return {
content: [
{ type: "text", text: `Weather in ${city}: 72F, clear skies` },
],
};
});
// Mount the streamable HTTP transport
app.post("/mcp", async (req, res) => {
const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: undefined, // stateless mode
});
await server.connect(transport);
await transport.handleRequest(req, res, req.body);
});
// Handle GET for SSE upgrades (optional, for clients that want server-initiated messages)
app.get("/mcp", async (req, res) => {
res.status(405).json({ error: "GET not supported in stateless mode" });
});
app.listen(3000, () => {
console.log("MCP server running on http://localhost:3000/mcp");
});
This gives you a working MCP server on a single /mcp endpoint. Clients like Claude Desktop and VS Code send POST requests with JSON-RPC payloads, and the server responds directly.
Python implementation
Install the SDK:
pip install mcp uvicorn
Create your server:
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("my-server")
@mcp.tool()
async def get_weather(city: str) -> str:
"""Get weather for a city."""
return f"Weather in {city}: 72F, clear skies"
if __name__ == "__main__":
mcp.run(transport="streamable-http", host="0.0.0.0", port=3000)
The Python SDK handles the HTTP server setup internally with Uvicorn. The transport="streamable-http" flag tells it to expose a single HTTP endpoint instead of using stdio.
Adding streaming responses
The basic setup returns a single JSON response per request. For long-running tools, you want to stream progress updates. Here is how that works in Node.js:
server.tool("analyze_repo", { url: { type: "string" } }, async ({ url }) => {
// The SDK handles upgrading to SSE when you yield progress notifications
return {
content: [
{ type: "text", text: `Analysis complete for ${url}. Found 12 files.` },
],
};
});
When a client sends Accept: text/event-stream in its request headers, the transport layer automatically switches to streaming mode. Your tool code does not change. The SDK negotiates the response format based on client capabilities.
For explicit progress notifications during a tool call, use the server’s notification mechanism:
server.tool("long_task", { input: { type: "string" } }, async ({ input }, { sendNotification }) => {
await sendNotification({ method: "notifications/progress", params: { progress: 25, total: 100 } });
// ... do work ...
await sendNotification({ method: "notifications/progress", params: { progress: 75, total: 100 } });
// ... finish ...
return { content: [{ type: "text", text: "Done" }] };
});
The transport handles serializing these as SSE events within the HTTP response. Clients that do not support streaming simply receive the final result.
Session management
Streamable HTTP supports two modes: stateless and stateful.
Stateless mode (shown above) creates a fresh server instance per request. No session tracking, no state between requests. This is the simplest option and works well for tools that do not need to maintain context across calls. It is ideal for serverless deployments where each invocation is isolated.
Stateful mode uses a session ID to maintain state across multiple requests:
const sessions = new Map();
app.post("/mcp", async (req, res) => {
const sessionId = req.headers["mcp-session-id"];
if (sessionId && sessions.has(sessionId)) {
const transport = sessions.get(sessionId);
await transport.handleRequest(req, res, req.body);
} else {
const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: () => crypto.randomUUID(),
});
sessions.set(transport.sessionId, transport);
const newServer = new McpServer({ name: "my-server", version: "1.0.0" });
// ... register tools ...
await newServer.connect(transport);
await transport.handleRequest(req, res, req.body);
}
});
The server returns an Mcp-Session-Id header in its first response. Clients include that header in subsequent requests to continue the session. Use stateful mode when your tools need to remember context between calls, like a database connection or a multi-step workflow.
Connecting clients
Add your server to Claude Desktop’s configuration:
{
"mcpServers": {
"my-server": {
"url": "http://localhost:3000/mcp"
}
}
}
For remote deployments, replace localhost with your server’s URL. Claude Desktop, VS Code Copilot, and other MCP clients that support streamable HTTP will send JSON-RPC requests to this endpoint automatically.
You can also test with curl:
curl -X POST http://localhost:3000/mcp \
-H "Content-Type: application/json" \
-d '{"jsonrpc": "2.0", "method": "tools/list", "id": 1}'
Deployment
Streamable HTTP works everywhere standard HTTP works. That includes:
- Railway / Render: Deploy as a normal web service. No special configuration needed.
- Cloudflare Workers: Works with the standard fetch handler. No long-lived connection workarounds required.
- VPS with nginx: Reverse proxy to your server’s port. No WebSocket or SSE-specific nginx configuration needed.
- AWS Lambda / Google Cloud Functions: Each request is independent in stateless mode, which fits the serverless execution model perfectly.
For production, add authentication and rate limiting. The MCP specification supports OAuth as the recommended auth mechanism for remote servers.
Common mistakes
Forgetting the Content-Type header. The transport expects application/json on incoming requests. If a client sends a request without the right Content-Type, the server should return a 415 status code. The SDK handles this, but custom middleware can interfere.
Using stateful mode on serverless. Stateful sessions require the same server instance to handle subsequent requests. Serverless platforms spin up new instances per request. If you need sessions on serverless, use an external session store like Redis, or stick with stateless mode.
Not handling the DELETE method. Clients send a DELETE request to /mcp to close a session. In stateless mode this is a no-op, but your route should still accept DELETE and return 200 to avoid client errors.
FAQ
Q: Can I still use SSE transport? A: The MCP specification marks SSE as deprecated. Existing SSE servers will keep working, but new servers should use streamable HTTP. The migration path is straightforward since the SDK handles most of the differences.
Q: Do all MCP clients support streamable HTTP? A: Claude Desktop, VS Code Copilot, and the official MCP client SDKs all support it. Older clients that only support SSE will not connect to a streamable HTTP server. If you need to support both, you can mount SSE and streamable HTTP on different paths.
Q: What is the performance difference? A: For simple tool calls, streamable HTTP has lower latency because there is no SSE connection setup. For streaming responses, performance is comparable to SSE since both use the same underlying event-stream format.