A stdio MCP server only exists while your host keeps its subprocess alive on the same machine. The moment you want a second client, a browser-based host, or a server that lives on a VPS, stdio runs out of road. Streamable HTTP is the transport that fixes that, and the switch is smaller than most people expect: expose one POST endpoint and change a single transport argument. In the official Python SDK that argument is mcp.run(transport="streamable-http").
Key Takeaways
- The short answer: build the server exactly as you would for stdio, then call
mcp.run(transport="streamable-http")(Python) or mount aStreamableHTTPServerTransport(TypeScript) on a single/mcpendpoint. - Streamable HTTP uses one HTTP endpoint that accepts POST; the server replies with either a JSON object or a per-request Server-Sent Events stream.
- Every client POST must send
Accept: application/json, text/event-streamand anMCP-Protocol-Versionheader, or the server rejects it. - Validate the
Originheader and bind to127.0.0.1locally; without both, a browser page can reach your server through DNS rebinding. - The 2026-07-28 spec revision removed the GET notification stream and protocol-level sessions, so a modern server answers GET and DELETE with 405.
What You Need Before You Start
This guide assumes a working tool server and a host that speaks HTTP MCP. Concretely:
- Python 3.10+ with the official
mcpSDK (pip install "mcp[cli]"), or Node.js 18+ with@modelcontextprotocol/sdkandexpress. - A host that connects over HTTP, such as Claude Code, the Anthropic API MCP connector, or the MCP Inspector for local testing.
- A free local port. The examples below use 8000 for Python and 3000 for TypeScript.
Serve Your Server Over Streamable HTTP in Five Steps
The whole procedure is five steps, and you can finish from this section alone. The Python path is the shortest, so start there.
- Install the SDK. The command-line extra pulls in the runner you need.
pip install "mcp[cli]" - Write the server with FastMCP. This is identical to a stdio server; the transport is not decided here.
from mcp.server.fastmcp import FastMCP mcp = FastMCP("weather", host="127.0.0.1", port=8000) @mcp.tool() def get_forecast(city: str) -> str: """Return a short forecast for a city.""" return f"Clear skies in {city}, 24C." - Choose the HTTP transport when you run it. One argument replaces stdio with Streamable HTTP.
if __name__ == "__main__": mcp.run(transport="streamable-http") - Start the process. The SDK serves the MCP endpoint at the
/mcppath on the host and port you set.python server.py # MCP endpoint now live at http://127.0.0.1:8000/mcp - Register the URL with your host. For Claude Code, add it with the HTTP transport flag.
claude mcp add --transport http weather http://127.0.0.1:8000/mcp
That is the full switch. The server that used to run as a child process now answers any client that can POST to the endpoint. The --transport http flag and its --transport sse legacy sibling are documented in the Claude Code MCP reference.
What Changed in the 2026-07-28 Transport Spec?
Streamable HTTP is not new, but its shape moved. The revision described in the 2026-07-28 changelog removed the standalone GET stream and protocol-level sessions that earlier revisions carried. That matters because a lot of older tutorials still show a Mcp-Session-Id handshake and a GET request for server-initiated messages.
Under the current Streamable HTTP specification, the model is simpler. The server exposes one endpoint that accepts POST. Each JSON-RPC request is its own POST, and the server answers with either Content-Type: application/json for a single object or Content-Type: text/event-stream when it wants to stream progress notifications before the final response. A modern server that receives a GET or DELETE on that endpoint returns 405 Method Not Allowed.
The client must include anAcceptheader listing bothapplication/jsonandtext/event-stream, and anMCP-Protocol-Versionheader whose value matches the version in the request body. A mismatch earns a400 Bad Requestwith aHeaderMismatcherror.
How Do the Three Transports Compare?
The choice comes down to who needs to reach the server and from where. This table lays out the trade-off.
| Transport | How the client connects | Multiple clients | Remote access | Status |
|---|---|---|---|---|
| stdio | Host launches the server as a subprocess and talks over stdin/stdout | No, one host per process | No, local only | Current, best for local tools |
| Streamable HTTP | Client POSTs each message to a single HTTP endpoint | Yes | Yes | Current, best for remote and shared servers |
| HTTP+SSE (2024-11-05) | Separate GET stream plus a POST endpoint | Yes | Yes | Deprecated since 2025-03-26, migrate off it |
If the server and host live on the same machine and only one host uses it, stdio stays the right call. Reach for Streamable HTTP the moment the answer to "who connects" is more than one process or a machine across the network.
Should You Run Stateless or Stateful?
The SDKs let you run either way, and the default you pick affects deployment. A stateless server treats every request as independent, which is what you want behind a load balancer or in a serverless function. In the official Python SDK you opt in when you construct the server.
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("weather", stateless_http=True, json_response=True)
Setting json_response=True tells the server to answer with a plain JSON body instead of opening an SSE stream, which is simpler for infrastructure that does not expect long-lived connections. Keep the default (stateful) only when a single instance holds per-client state in memory.
How Do You Wire the HTTP Server Into a Host in TypeScript?
The Node path takes a few more lines because you mount the transport on your own HTTP server. The pattern below is stateless: it builds a fresh server and transport per request, which sidesteps request-id collisions. It uses the StreamableHTTPServerTransport from the official TypeScript SDK.
import express from "express";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
const app = express();
app.use(express.json());
app.post("/mcp", async (req, res) => {
const server = new McpServer({ name: "weather", version: "1.0.0" });
server.registerTool(
"get_forecast",
{ description: "Return a short forecast for a city." },
async ({ city }) => ({ content: [{ type: "text", text: `Clear skies in ${city}.` }] })
);
const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: undefined, // stateless mode
enableDnsRebindingProtection: true,
allowedHosts: ["127.0.0.1"],
});
res.on("close", () => { transport.close(); server.close(); });
await server.connect(transport);
await transport.handleRequest(req, res, req.body);
});
app.listen(3000, () => console.log("MCP on http://127.0.0.1:3000/mcp"));
If you built your first server with the TypeScript SDK over stdio, this is the one file that changes; the tool definitions are the same. See the walkthrough on building a TypeScript MCP server with the official SDK if you need that foundation first.
Troubleshooting the Errors You Will Hit First
Four failures account for most of the time lost here. Match the symptom to the fix.
The Host Never Detects the Server
Almost always the URL points at the root instead of the endpoint. The SDK serves MCP at /mcp, not /. Point the host at http://127.0.0.1:8000/mcp, then confirm the process is actually listening on that port.
The Connection Returns 403 Forbidden
Origin validation is doing its job. The spec requires the server to reject requests whose Origin header is present and untrusted, which is the DNS rebinding defense. When a legitimate host trips it, add that host to the transport's allowedHosts and allowedOrigins rather than disabling the check.
Requests Fail With 400 Bad Request
This is usually a header problem. Either the client omitted MCP-Protocol-Version, or its value does not match the version in the request body, which triggers the HeaderMismatch error code -32020. Make sure the client and server agree on a protocol revision.
An Old Client Expects Sessions or a GET Stream
A client written against 2025-03-26 may send a Mcp-Session-Id header or open a GET stream, both of which the 2026-07-28 revision dropped. A server that speaks only the new revision answers GET and DELETE with 405 and ignores session headers. Upgrade the client, or run a server build that still supports the older revision for that counterpart.
What to Do Next
With the endpoint live, tighten it before anything else touches it.
- Test the endpoint with the MCP Inspector before wiring it into a host; the process for that is covered in debugging an MCP server with MCP Inspector.
- Add authentication for any server reachable off localhost. The spec calls for proper auth on all connections, so put OAuth 2.1 or a bearer token in front of a remote deployment.
- When you deploy behind nginx, set
X-Accel-Buffering: noon SSE responses so the proxy stops buffering stream events. - If you are still choosing your build stack, the FastMCP server guide covers tools, resources, and prompts before you expose any of them over the network.