Home Blog Resume Contact Ask AI About Me
Home/Blog/How to Serve MCP Over Streamable HTTP Instead…
How toLLM EngineeringMCPStreamable HTTPtransports

How to Serve MCP Over Streamable HTTP Instead of stdio

9 min readBy Miloš Mitrović

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 a StreamableHTTPServerTransport (TypeScript) on a single /mcp endpoint.
  • 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-stream and an MCP-Protocol-Version header, or the server rejects it.
  • Validate the Origin header and bind to 127.0.0.1 locally; 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 mcp SDK (pip install "mcp[cli]"), or Node.js 18+ with @modelcontextprotocol/sdk and express.
  • 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.

  1. Install the SDK. The command-line extra pulls in the runner you need.
    pip install "mcp[cli]"
  2. 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."
  3. Choose the HTTP transport when you run it. One argument replaces stdio with Streamable HTTP.
    if __name__ == "__main__":
        mcp.run(transport="streamable-http")
  4. Start the process. The SDK serves the MCP endpoint at the /mcp path on the host and port you set.
    python server.py
    # MCP endpoint now live at http://127.0.0.1:8000/mcp
  5. 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 an Accept header listing both application/json and text/event-stream, and an MCP-Protocol-Version header whose value matches the version in the request body. A mismatch earns a 400 Bad Request with a HeaderMismatch error.

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.

TransportHow the client connectsMultiple clientsRemote accessStatus
stdioHost launches the server as a subprocess and talks over stdin/stdoutNo, one host per processNo, local onlyCurrent, best for local tools
Streamable HTTPClient POSTs each message to a single HTTP endpointYesYesCurrent, best for remote and shared servers
HTTP+SSE (2024-11-05)Separate GET stream plus a POST endpointYesYesDeprecated 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: no on 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.

Sources

M
Miloš Mitrović
Revenue Operations & AI Automation

Have a question or a project?

Whether it is about this post or a system you want built, I'm happy to talk.

Get in touch

404

Post not found. It may have been moved or the link is incorrect.

← Back to the blog
Ask AI About Me
Clicking an assistant copies the prompt and opens it: ready to run in ChatGPT, Perplexity, and Grok; in Claude, Gemini, or Copilot press Ctrl+V (Cmd+V on Mac) to paste. Use Copy prompt for any other AI. The assistant reads my site, so it needs web access.
Summarize with AI
ChatGPT, Perplexity, and Grok open with the prompt ready to run. Claude, Gemini, and Copilot open a chat with the prompt copied; press Ctrl+V (Cmd+V on Mac) to paste. The full text is included, so it works even without web access.