MCP over HTTP: transports, sessions and what breaks
The Streamable HTTP transport changed in revision 2026-07-28: the GET stream endpoint and protocol-level sessions are gone. Here is what a request looks like now and what stops working.

The Model Context Protocol defines two transports: stdio, which is newline-delimited JSON-RPC over a byte stream, and Streamable HTTP, where the server exposes a single HTTP endpoint that accepts POST. Protocol revision 2026-07-28 changed Streamable HTTP in two ways that break existing clients: it removed the GET stream endpoint, and it removed protocol-level sessions. A client that opens a long-lived GET stream, or that carries an Mcp-Session-Id header from an initialize handshake, is written against an earlier revision and needs a compatibility path.
Under the Streamable HTTP transport, every JSON-RPC message the client sends is its own HTTP POST to the MCP endpoint. The body is a single JSON-RPC request or notification, never a batch and never a response. The client must send an Accept header listing both application/json and text/event-stream, because the server chooses: it answers a request with either one JSON object or a Server-Sent Events stream scoped to that request, carrying request-related notifications and then the final response. A notification the server accepts gets HTTP 202 Accepted with no body.
The Streamable HTTP transport also requires request metadata in headers. MCP-Protocol-Version is required on every POST, and its value must match the io.modelcontextprotocol/protocolVersion field inside the body's _meta. Mcp-Method carries the JSON-RPC method on all requests, and Mcp-Name carries params.name or params.uri on tools/call, resources/read and prompts/get. The headers exist so that a proxy, a gateway or an audit log can route and record a call without parsing the JSON body.
Protocol-level sessions were removed in revision 2026-07-28. Earlier revisions established a connection-scoped session: the server could assign an ID at initialization by returning an Mcp-Session-Id header, clients then had to send that header on every later request, a server that required one answered 400 Bad Request when it was missing, and it answered 404 Not Found once it had terminated the session, which was the client's signal to start a new one. Clients could end a session with an HTTP DELETE. None of that is part of the current revision.
The work those sessions did has moved rather than disappeared. Server-to-client interactions such as sampling, elicitation and roots are now embedded in results as input requests under the Multi Round-Trip Requests extension, and long-lived change notifications such as list changes and resource updates are delivered on the response stream of a subscriptions/listen request. So a server still streams to a client; it does so on a stream the client opened for that purpose, rather than on a connection-scoped channel the transport maintained.

Dynamic client registration is the one part of MCP's authorization story marked SHOULD rather than MUST. What the specification actually requires, the two alternatives it names for servers that skip registration, and what implementing it costs.

Market4 turns one release note into a changelog page, a blog post, a mail-out and a week of social posts — and then tells you which of them brought anyone back.
| Behaviour | Revision 2025-06-18 | Revision 2026-07-28 |
|---|---|---|
| Server-initiated stream | HTTP GET on the MCP endpoint, or HTTP 405 if unsupported | Removed; notifications arrive on a subscriptions/listen response stream |
| Session identity | Optional Mcp-Session-Id assigned at initialize | Removed at the protocol level |
| Protocol version on the wire | MCP-Protocol-Version header after initialization | MCP-Protocol-Version header on every POST, matching _meta in the body |
| Version mismatch | 400 Bad Request for an unsupported version | 400 Bad Request with an UnsupportedProtocolVersionError listing supported versions |
| Unknown method | JSON-RPC error in a 200 response | 404 Not Found with JSON-RPC error -32601 |
The Streamable HTTP transport carries three requirements that are easy to skip on a local server and expensive to skip on a hosted one. Servers must validate the Origin header on all incoming connections to prevent DNS rebinding attacks, and must answer HTTP 403 Forbidden when an Origin header is present and invalid. A server running locally should bind to 127.0.0.1 rather than to all interfaces. And a server should authenticate every connection, which for a hosted MCP server means the authorisation flow rather than a shared secret in a query string.
Backward compatibility in MCP is a decision, not a default. The specification allows a server that supports clients older than 2025-06-18 to treat a request with no MCP-Protocol-Version header as protocol version 2025-03-26; a server that does not want those clients must reject the request instead. Both are conforming. The question to answer before writing any code is which client versions are actually connecting, because supporting a removed feature means keeping a second code path alive for as long as the oldest client you tolerate.
stdio suits a server that runs on the same machine as the client and is started by it, because the client owns the process lifecycle and no network is involved. Streamable HTTP suits a server that runs somewhere else and serves many clients, which also means it needs authorisation, Origin validation and a public address. The protocol says clients should support stdio whenever possible, and the two are not exclusive: one codebase can expose both.
By closing the SSE response stream. In the current revision the notifications/cancelled message is used only on the stdio transport; over Streamable HTTP, closing the stream is itself the cancellation signal and no cancellation message is expected. A server therefore needs to notice a closed response stream and stop the work behind it, rather than waiting for an explicit message that will not arrive.
The server's own storage, keyed by whatever identity its authorisation layer establishes. Protocol-level sessions were removed in revision 2026-07-28, so the transport no longer carries an identifier for the client to echo. State that used to hang off a session, such as a selected workspace or an in-progress job, has to be addressed by an identifier the server issues in a result and the client passes back as an ordinary tool parameter.
Two rules produce a 400 on a request that parses. The MCP-Protocol-Version header must match the io.modelcontextprotocol/protocolVersion value in the body's _meta, and a mismatch is answered with a HeaderMismatch error. A version the server does not implement is answered with an UnsupportedProtocolVersionError that lists the versions it does support. Read the JSON-RPC error body rather than the status, because both cases share the status.
When an assistant works through dozens of your tools in a single turn, there is no transaction around them. Everything before the failure has already happened, and the turn can still end by reporting success.

Connecting a remote MCP server to Claude, ChatGPT or Cursor is the same URL in all three. What differs is where the URL goes, whether the client can complete a browser sign-in, and what counts as proof that it worked.