MCP has had three remote transports in under two years, and most guides still describe the middle one. The original HTTP+SSE transport (2024-11-05) used two endpoints and a long-lived event stream. Streamable HTTP replaced it in 2025-03-26 with a single endpoint, optional sessions and resumable streams. The current revision, 2026-07-28, kept the name Streamable HTTP but took out sessions, the GET stream and resumability, so every request now stands on its own.

This guide covers all three, shows what each looks like on the wire, and gives the migration steps for an old SSE server and for a 2025-era Streamable HTTP server. Spec text, SDK docs and client docs were checked on October 6, 2026. The real-world example is our own server, mcp.dialmcp.com, which is partway through this transition.

SSE the format is not HTTP+SSE the transport

Bundles of yellow and orange network patch cables routed through black cable managers on a numbered patch panel
Photo: dmitrybarsky / Flickr (CC BY 2.0), cropped.

Most confusion in "MCP SSE vs Streamable HTTP" threads comes from one word doing two jobs.

  • Server-Sent Events is a response format: a text/event-stream body that carries several messages over one HTTP response. Streamable HTTP still uses it. In 2026-07-28, the server "answers each request with either a single JSON object or a Server-Sent Events (SSE) stream scoped to that request."
  • HTTP+SSE is the 2024-11-05 transport. The client opened a permanent SSE connection on one endpoint and posted its messages to a second one. This is the thing that is deprecated.

So "SSE is deprecated" is half right. The two-endpoint transport has been deprecated since 2025-03-26, and 2026-07-28 formally reclassified it as Deprecated under the spec's new feature lifecycle policy. The spec says new implementations "SHOULD NOT adopt it" and existing ones "SHOULD migrate to Streamable HTTP." It has not been removed yet. The deprecated-features registry lists its earliest removal as "Three months after SEP-2596 reaches Final." The event-stream format is staying, and your Streamable HTTP server may answer with one today.

Three generations of the remote transport

stdio, the local transport where the client launches your server as a subprocess, has barely changed since 2024. The remote transport has changed with almost every revision. Here is the whole history in one table:

HTTP+SSE (2024-11-05)Streamable HTTP (2025-03-26 to 2025-11-25)Streamable HTTP (2026-07-28)
EndpointsTwo: an SSE endpoint plus a POST endpoint announced in an endpoint eventOne MCP endpoint, POST and GETOne MCP endpoint, POST only
Reply to a requestAlways over the open SSE connectionJSON or a request-scoped SSE streamJSON or a request-scoped SSE stream
Server-to-client streamThe permanent SSE connectionOptional GET streamRemoved; change notifications use subscriptions/listen
HandshakeinitializeinitializeNone; version and capabilities ride in every request's _meta
SessionsImplied by the connectionOptional Mcp-Session-Id, ended with DELETERemoved
ResumabilityNoneLast-Event-ID on a GETRemoved
Version headerNoneMCP-Protocol-Version from 2025-06-18MCP-Protocol-Version and Mcp-Method on every POST; Mcp-Name on tool, resource and prompt calls
Client closes the streamConnection lost"SHOULD NOT" be read as cancellation"MUST" be treated as cancellation

Two smaller changes fill in the gaps. JSON-RPC batching arrived in 2025-03-26 and left again in 2025-06-18. The 2025-11-25 revision made servers answer a bad Origin header with 403 and let them close SSE connections so that clients poll and resume. That polling went away with resumability eight months later.

What each transport looks like on the wire

HTTP+SSE (2024-11-05)

The client opens a GET and keeps it open. The server's first event tells the client where to post. The spec does not fix the paths, but the TypeScript SDK's legacy example used /sse and /messages, and many servers followed it.

GET /sse
Accept: text/event-stream

event: endpoint
data: /messages?sessionId=abc123

POST /messages?sessionId=abc123
{"jsonrpc":"2.0","id":1,"method":"tools/list"}

event: message                  <- the reply arrives on the GET stream
data: {"jsonrpc":"2.0","id":1,"result":{"tools":[...]}}

Every reply depends on one long-lived connection to one specific server process. If a proxy buffers that stream or a load balancer routes the POST somewhere else, the reply never shows up.

Streamable HTTP with sessions (2025)

Everything goes to one endpoint. The client must send an Accept header listing both application/json and text/event-stream, and the reply comes back on the same HTTP response. The server may hand out a session ID with the initialize result, and the client then repeats it on every request.

POST /mcp
Accept: application/json, text/event-stream
{"jsonrpc":"2.0","id":1,"method":"initialize","params":{...}}

HTTP/1.1 200 OK
Mcp-Session-Id: 1868a90c...
Content-Type: application/json

POST /mcp
Mcp-Session-Id: 1868a90c...
MCP-Protocol-Version: 2025-06-18
{"jsonrpc":"2.0","id":2,"method":"tools/list"}

A GET to the same URL could open a standalone stream for server-initiated messages, a DELETE ended the session, and a server that had dropped the session had to answer 404 so the client would initialize again.

Streamable HTTP, stateless (2026-07-28)

There is no handshake. Each POST carries the protocol version in a header and inside _meta, together with headers that name the method and the tool, so gateways can route without parsing the body. This is the spec's own example:

POST /mcp HTTP/1.1
Content-Type: application/json
MCP-Protocol-Version: 2026-07-28
Mcp-Method: tools/call
Mcp-Name: get_weather

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "get_weather",
    "arguments": { "location": "Seattle, WA" },
    "_meta": {
      "io.modelcontextprotocol/protocolVersion": "2026-07-28",
      "io.modelcontextprotocol/clientInfo": { "name": "ExampleClient", "version": "1.0.0" },
      "io.modelcontextprotocol/clientCapabilities": {}
    }
  }
}

If the header and the body disagree, the server rejects the request with 400 and JSON-RPC error -32020 (HeaderMismatch). An unsupported version gets a 400 with -32022 and a list of the versions the server does support. The new server/discover method returns supported versions, capabilities and server info up front. Servers must implement it, and clients may call it, though nothing requires them to.

Why sessions were removed, and what replaces them

The reasoning is in SEP-2575, the proposal behind the change: "Placing an MCP server behind a standard load balancer, for example, is challenging because a client's session is coupled to the specific server instance holding its state." Operators were "forced to implement complex and fragile solutions like sticky sessions" just to keep a protocol-level ID alive. The companion proposal, SEP-2567, also notes that clients scoped sessions inconsistently and almost never resumed them. It says ChatGPT creates a fresh session for every individual tool call.

The replacements are plain and mostly live in your tool design:

  • State handles. If a workflow needs state across calls, a tool returns an ID and later calls pass it back as an ordinary argument. The SEP's example is create_basket() returning a basket_id that add_item(basket_id, ...) takes. There is no wire format for this; it is just a tool-design pattern. The 2026-07-28 security guidance adds that servers "MUST NOT treat possession of a state handle as authentication," so bind each handle to the user in the verified token.
  • Request-scoped streams. Progress and log notifications travel on the SSE response of the request they belong to, and the final response should end the stream.
  • subscriptions/listen. A client that wants list-changed or resource-updated notifications opens one long-lived POST whose response is an SSE stream. It replaces both the GET endpoint and resources/subscribe.
  • Tasks for durable work. With resumability gone, a dropped stream loses the in-flight request and the client must re-issue it with a new ID. Work that has to survive that belongs in the tasks extension (io.modelcontextprotocol/tasks), which 2026-07-28 moved out of the core protocol.

One behavior flips completely. In 2025 a client disconnecting "SHOULD NOT" be read as a cancellation, and clients sent an explicit cancel notification. In 2026-07-28, closing the response stream is the cancellation. Long tool handlers should check for it and stop work they no longer need to finish.

Migrating an HTTP+SSE server to Streamable HTTP

If you still run the two-endpoint transport, the upstream SDKs have already moved. As of October 2026:

  • Python (mcp 2.x on PyPI) still runs SSE through mcp.run(transport="sse"), but its docs say "Don't build anything new on it." Version 2 also renamed FastMCP to MCPServer, so importing the old module path raises ModuleNotFoundError.
  • TypeScript split into new packages. @modelcontextprotocol/sdk is now the 1.x maintenance line, which stops at spec 2025-11-25. Version 2 lives in @modelcontextprotocol/server and @modelcontextprotocol/client. The v2 server "never serves the HTTP+SSE transport"; a frozen copy sits in @modelcontextprotocol/server-legacy/sse, planned for removal in v3.

The Python change is one argument:

from mcp.server import MCPServer

mcp = MCPServer("notes")

@mcp.tool()
def add_note(text: str) -> str:
    """Save a note."""
    return f"Saved: {text}"

if __name__ == "__main__":
    mcp.run(transport="streamable-http", port=3001)   # was transport="sse"

That serves http://127.0.0.1:3001/mcp. In v2, transport options go to run(), not the constructor. The same streamable_http_app() answers both 2026 clients and 2025 clients, routed by the MCP-Protocol-Version header. In TypeScript v2, a factory builds a fresh server per request:

import { createMcpHandler, McpServer } from '@modelcontextprotocol/server';
import * as z from 'zod/v4';

const handler = createMcpHandler(() => {
  const server = new McpServer({ name: 'notes', version: '1.0.0' });
  server.registerTool(
    'add-note',
    { description: 'Save a note', inputSchema: z.object({ text: z.string() }) },
    async ({ text }) => ({ content: [{ type: 'text', text: `Saved: ${text}` }] })
  );
  return server;
});
// Cloudflare Workers, Deno, Bun: export default handler

Then handle the people already pointed at your old URL. Large vendors have shown the pattern: publish the new endpoint, keep the old one running for a while, and announce a date. Atlassian kept https://mcp.atlassian.com/v1/sse "available for backward compatibility until 30 June 2026" and pointed users at a /v1/mcp endpoint. Linear announced on February 5, 2026 that it was "fully removing SSE support" and told users to switch from /sse to /mcp. Many clients probe for you: VS Code "first tries the HTTP Stream transport and falls back to SSE," and Claude Code does the same from v2.1.265.

On the client side, the TypeScript docs show the fallback directly. Try Streamable HTTP first, and use SSE only if that fails:

try {
  const client = new Client({ name: 'my-client', version: '1.0.0' });
  await client.connect(new StreamableHTTPClientTransport(new URL(url)));
  return client;
} catch {
  const client = new Client({ name: 'my-client', version: '1.0.0' });
  await client.connect(new SSEClientTransport(new URL(url)));
  return client;
}

Moving a 2025 Streamable HTTP server to 2026-07-28

This is the migration most current tutorials miss, because the transport name did not change. Pulled together from the spec, the checklist for a server that only speaks 2026-07-28 is short:

  • Answer GET and DELETE on the MCP endpoint with 405 Method Not Allowed.
  • Ignore any Mcp-Session-Id header, and do not mint or echo session IDs.
  • Ignore Last-Event-ID; streams are not resumable.
  • Implement server/discover.
  • Require MCP-Protocol-Version, Mcp-Method and, for tools, resources and prompts, Mcp-Name. Reject mismatches with -32020.
  • Send X-Accel-Buffering: no when you open an SSE stream, so nginx-style proxies do not hold your events back.
  • Move anything you kept "per session" into explicit handles or tasks.

Then decide whether you are modern-only or dual-era. The spec's compatibility matrix is blunt about it: a legacy client against a modern-only server "Fails," because "Legacy clients have no fall-forward mechanism." A dual-era server serves both. It handles a request carrying 2026 _meta statelessly, and an initialize request gets the old session behavior. Both SDKs do this for you, with one asymmetry worth knowing. Python v2 always serves both eras, and its Client defaults to auto-negotiation. TypeScript v2 serves legacy clients statelessly by default (legacy: 'reject' makes it modern-only), but its Client defaults to the legacy handshake, and the docs state that "Nothing in v2 puts a 2026-07-28 byte on the wire by default."

Two naming traps cause trouble here. The 2025-era "stateless mode" (sessionIdGenerator: undefined in TypeScript, stateless_http=True in Python) is an SDK setting on the old protocol. It is not 2026-07-28 support, and in Python v2 it "only touches the legacy leg." And a dual-era client finds out which era a server speaks by sending a 2026 request first. It should look at the error body before falling back, because a modern server also answers 400 for a wrong version. Cursor's forum shows what happens when a client gets this wrong: a Cursor team member wrote that "the SSE fallback you saw is our client misreading the 400 as 'Streamable HTTP not supported'" when the server was modern-only.

Which clients speak which transport (October 2026)

Pulled from each vendor's own documentation on October 6, 2026. "Not stated" means the docs say nothing either way, not that the feature is missing.

ClientStreamable HTTPHTTP+SSE2026-07-28
Claude CodeYes (--transport http, recommended)Deprecated, auto-fallback from v2.1.265Yes, on its newer SDK runtime; asks each HTTP server first
Claude custom connectorsYesSupported, "being deprecated"; a URL ending in /sse selects itNot stated
CursorYes (url in mcp.json)YesNo, per a Cursor forum reply on 2026-09-21; asked on 2026-09-25 about a date: "No timeline at the moment!"
VS CodeYes ("type": "http")Yes ("type": "sse"), as fallbackNot stated
CodexYesNot listed (docs list stdio and Streamable HTTP)Not stated
ChatGPT plugins (public submission)RequiredNot listedNot stated
OpenAI Responses APIYesYesNot stated
Windsurf (Devin Desktop)YesYesNot stated

The practical reading: build on Streamable HTTP, keep serving 2025-era clients for now, and do not ship a modern-only server if your users are on Cursor. Per-client setup for a remote URL is in our client setup docs, and MCP configuration examples has the JSON for each file.

A real server in the middle of the transition

DialMCP is a remote MCP server that lets an agent place real phone calls. Its endpoint is a good example of where most production servers sit today, because it was built in July 2026, weeks before the 2026-07-28 revision shipped, on an SDK release that predates it, and it has not moved to 2026-07-28 yet.

It is Streamable HTTP only. https://mcp.dialmcp.com/mcp answers POST, GET and DELETE, each with a 401 and an OAuth challenge until the client has a token, and https://mcp.dialmcp.com/sse returns 404. There was never an SSE endpoint to retire. The server runs on Cloudflare Workers with the Agents SDK's McpAgent.serve("/mcp") on top of the 1.x TypeScript SDK, and it uses sessions in the 2025 sense. On initialize it mints an Mcp-Session-Id and routes that session to its own Durable Object, and it rejects any later request that arrives without the header. The SDK version it pins negotiates protocol versions up to 2025-06-18.

That design shows why the spec dropped sessions, and also why some servers could live with them. Durable Objects are addressed by name, so every request carrying a session ID reaches the same instance without a sticky load balancer. That routing is the hard part SEP-2575 describes, and the platform took care of it here. Most deployments do not get that for free.

Clients connect to DialMCP the way the compatibility matrix predicts. Claude, ChatGPT, Codex, Cursor and VS Code use the 2025 handshake, and a dual-era client such as Claude Code falls back to it after the server rejects a 2026-style request. Moving to 2026-07-28 would mean serving both eras from one endpoint. The call flow already fits the new model, because each call has an explicit ID: place_call returns a call_id, and get_call and end_call take it. That is a state handle, and it is checked against the signed-in user rather than trusted on sight. Nothing about the phone call itself depends on the transport. Calls still go out from the user's own verified number, with server-side AI disclosure and enforced calling-hours limits, whichever revision the client speaks.

Troubleshooting transport errors

SymptomLikely causeFix
406 Not Acceptable on POSTThe client's Accept header lacks application/json or text/event-streamSend both. This is a MUST for every Streamable HTTP client.
400 "Mcp-Session-Id header is required"A 2025 sessionful server received a request without initialize first, often from a 2026 clientUse a dual-era client, or make the server dual-era
404 "Session not found" after a deploy or scale-outSessions held in process memory; a different worker or a restarted one answeredSticky routing, a shared store, or drop sessions and move to 2026-07-28
405 on GETThe server is modern-only, or never offered a GET streamExpected; use subscriptions/listen for change notifications
400 with -32022The server does not support the requested protocol versionRetry with a version from the error's supported list
400 with -32020MCP-Protocol-Version, Mcp-Method or Mcp-Name disagrees with the bodyFix the client, or a proxy that rewrites headers
403 Forbidden on a local serverThe Origin header failed validationAdd the origin to the allowlist rather than turning the check off
421 Misdirected Request from a Python server behind a real hostnameThe Python SDK's DNS-rebinding protection only allows localhost Host headers by defaultPass transport_security with allowed_hosts and allowed_origins. Our Docker deploy guide walks through it.
Progress events arrive all at once at the endA reverse proxy is buffering the SSE responseSend X-Accel-Buffering: no and disable buffering for the route
Client silently falls back to SSE against a new serverThe client treated a modern 400 as "legacy server"Upgrade the client; check the 400 body, not just the status

For seeing the raw requests, MCP Inspector connects over either transport and shows each message. If the 401 is the first thing you hit, that is authorization rather than transport; MCP authorization and OAuth traces that flow step by step.

Further reading

Try a Streamable HTTP server that does something real. Add https://mcp.dialmcp.com/mcp to Claude, ChatGPT or Codex, sign in, verify your own mobile number, and your agent can place real phone calls with a transcript and a structured outcome. Free during launch.

Connect DialMCP to your client

FAQ

What is MCP Streamable HTTP?

It is the remote transport in the Model Context Protocol spec. The client sends JSON-RPC messages as HTTP POST requests to a single MCP endpoint, and the server answers each one with either a JSON object or a Server-Sent Events stream scoped to that request. It replaced the HTTP+SSE transport in the 2025-03-26 revision.

Is MCP SSE deprecated?

The HTTP+SSE transport, with its separate SSE and POST endpoints, is deprecated, and new servers should not adopt it. It has not been removed from the spec yet. Server-Sent Events as a response format is not deprecated: Streamable HTTP servers still use it to stream progress and results.

What is the difference between Streamable HTTP and SSE in MCP?

HTTP+SSE needed a long-lived GET connection for every reply and a second endpoint for client messages, which ties each client to one server process. Streamable HTTP uses one endpoint and returns each reply on the response to the request that asked for it, so it works with ordinary load balancers and serverless platforms.

Does MCP Streamable HTTP still use sessions?

Not in the current 2026-07-28 revision. It removed the Mcp-Session-Id header, the initialize handshake and the GET stream, so every request is self-contained. Servers that need state across calls return an explicit handle from a tool and accept it as an argument later. The 2025 revisions still support sessions, and many servers run them today.

How do I run a Python MCP server over Streamable HTTP?

With the official mcp package version 2, create an MCPServer, register tools, and call mcp.run(transport="streamable-http"). It serves http://127.0.0.1:8000/mcp by default. Behind a real hostname, configure allowed hosts and origins, or the SDK's DNS-rebinding protection answers 421.

Which MCP clients support Streamable HTTP?

As of October 2026, Claude Code, Claude connectors, Cursor, VS Code, Codex, ChatGPT, the OpenAI Responses API and Windsurf all document Streamable HTTP. Support for the stateless 2026-07-28 revision is less complete: Claude Code negotiates it, and Cursor said in September 2026 that it does not support it yet.