skip to content

Streamable HTTP Transport

One HTTP endpoint takes POST only and answers with either a JSON object or an SSE stream, under required MCP-Protocol-Version, Mcp-Method and Mcp-Name headers. Interviewers ask where GET went.

part ofAPI stylesoverview, primer and where to startread it →
on this pageshow

questions

5

In MCP's Streamable HTTP transport, what does a client POST and what can a server reply?

level: juniorimportance: must knowfreq 72%

answer

  1. one URL, one verb
  2. the client offers two media types
  3. the server picks the reply shape
  4. no body for a message with no id
  5. POST in, JSON or SSE out

basics

~20 s

MCP's Streamable HTTP transport exposes a single endpoint that accepts POST only. The client POSTs one JSON-RPC message with an Accept header listing both application/json and text/event-stream, and the server answers with either a JSON body or an SSE stream.

solid answer

~40 s

In MCP revision 2026-07-28 the remote binding is **Streamable HTTP**: one URL, one method. The client sends `POST` with a single JSON-RPC message as the body and an `Accept` header that lists **both** `application/json` and `text/event-stream`, because the client cannot know in advance which shape the server will choose. The server decides per request: for a short answer it replies `Content-Type: application/json` with the JSON-RPC response object; for a long-running call it makes that same response an SSE stream (`Content-Type: text/event-stream`) carrying progress notifications and finally the response. A JSON-RPC notification, which has no id and expects no answer, gets `202 Accepted` with no body. There is no separate streaming URL and no second endpoint — one POST in, one response (possibly streamed) out.

code

http · 8 lines
http
POST /mcp HTTP/1.1
Host: example.com
Content-Type: application/json
Accept: application/json, text/event-stream
MCP-Protocol-Version: 2026-07-28
Mcp-Method: tools/list

{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientCapabilities":{}}}}

go deeper

for a junior

Be able to say plainly: one endpoint, POST only, Accept lists application/json and text/event-stream, and the reply is either a JSON body or an SSE stream.

for a middle

Explain that the server picks the reply shape per request, that the SSE stream is scoped to that one request and ends with the response, and that a notification gets 202 with no body.

for a senior

Show you have deployed it: response-buffering proxies silently swallow progress events, HTTP clients that buffer whole bodies break streaming, and per-request streams mean idle timeouts hit long tool calls.

for a principal

Own the argument for why a single POST endpoint was the right design: it survives ordinary gateways, WAFs and load balancers untouched, and it keeps the transport's surface small enough to reason about for authorization and audit.

## The shape in one sentence Streamable HTTP is MCP's remote transport as of revision 2026-07-28: **one HTTP endpoint that accepts POST and nothing else**, where every exchange is client-initiated and the response may be either a plain JSON body or a Server-Sent Events stream, chosen by the server for that single request. ## What the client sends The client makes an ordinary HTTP `POST` to the MCP endpoint URL. The body is exactly one JSON-RPC message — a request such as `tools/call`, `tools/list` or `resources/read`, or a notification such as `notifications/cancelled`. The `Accept` header must offer both media types: `Accept: application/json, text/event-stream` This is not decoration. The client is telling the server "either shape is fine", because the choice belongs to the server and is made after it has looked at the request. A client that offers only `application/json` has told the server it cannot receive a stream, and a server that needs to stream has nowhere to go. Because MCP is stateless in this revision, the POST also carries everything the server needs to interpret it: the protocol version and the client's capabilities travel inside `params._meta`, and are mirrored into HTTP headers so that infrastructure in front of the server can route without parsing the body. ## What the server replies The server has three outcomes: 1. **A JSON response.** `200 OK`, `Content-Type: application/json`, body is the JSON-RPC response object for the request that was posted. This is the normal answer for anything that completes quickly — `tools/list`, `server/discover`, a fast tool call. 2. **An SSE-upgraded stream.** `200 OK`, `Content-Type: text/event-stream`. The response body becomes an event stream on which the server can send request-scoped notifications — `notifications/progress`, `notifications/message` — and then the final JSON-RPC response, after which the stream ends. Crucially this stream belongs to *that request*: it is not a general-purpose push channel, and messages for other requests do not appear on it. 3. **`202 Accepted` with no body.** This is the answer to a JSON-RPC notification. A notification has no `id`, so by definition there is no response to return; `202` says "received, nothing to answer". ## Why one endpoint and one method Earlier MCP designs had a second channel: a long-lived `GET` stream the client opened to receive server-initiated traffic. That is gone in 2026-07-28. Server-to-client pushes that are not tied to a particular request now ride on a dedicated `subscriptions/listen` call — which is itself just another POST whose response happens to be a long-lived stream. The result is that the transport has exactly one verb and one URL, which makes it trivially deployable behind ordinary HTTP infrastructure: a load balancer, an API gateway or a WAF sees nothing but POSTs to a single path. A server that implements only the 2026-07-28 revision therefore SHOULD answer `GET` or `DELETE` on the endpoint with `405 Method Not Allowed`. ## Why the server, not the client, picks the body shape The server is the only party that knows whether the work will take 20 milliseconds or 20 seconds, and whether it will have anything to report along the way. Letting it decide per request avoids two bad alternatives: forcing every response through SSE (wasteful for a `tools/list` that is one small JSON object, and hostile to caching intermediaries), or forcing everything into a single JSON body (which means a two-minute tool call reports nothing until it finishes and looks like a hung connection to every proxy in the path). ## Practical consequences - An HTTP client library that buffers responses fully before returning is unusable for the streaming case; you need one that exposes the body incrementally. - Intermediaries that buffer response bodies (some reverse-proxy defaults do) will hold the SSE frames until the request completes, destroying progress reporting without producing an error anywhere. - Because the stream is per-request, closing it has a specific meaning to the server: it is a cancellation of that request. - Because every POST is self-contained, any server instance can serve any request; there is no affinity requirement introduced by the transport itself. ## What this looks like end to end A client calling a slow tool posts one message, gets `200` with `text/event-stream`, reads several `notifications/progress` messages, reads the final `tools/call` response, and the server closes the stream. A client listing tools posts one message and gets `200` with `application/json` and the whole result at once. Both used the same URL and the same method.

  • What happens if the client's Accept header omits text/event-stream?
    It has declared that it cannot consume a stream, so the server has no legal way to upgrade that response. The MCP spec requires the client to list both `application/json` and `text/event-stream`; omitting one is a malformed request from the transport's point of view, and a server is entitled to reject it rather than guess. In practice you lose progress reporting on every long call.
  • If the stream is per request, how does a server push a tools list change that belongs to no request?
    Not on a request stream. The client opens a dedicated long-lived POST to `subscriptions/listen` and names the change classes it wants; those notifications arrive on that stream, tagged with a subscription id. Request-scoped notifications such as `notifications/progress` still travel on the originating request's own response stream, never on the listen stream.
  • Why 202 rather than 204 for a notification?
    `202 Accepted` says the message was received and will be acted on, without asserting that processing finished — which is exactly the semantics of a JSON-RPC notification, where the sender is not entitled to a result. MCP specifies `202` with no body for this case, so a client should treat any 2xx-with-body reply to a notification as a server bug rather than something to parse.

It is like a service counter that takes written slips only: you hand in one slip, and the clerk either hands you the answer immediately or keeps talking to you at the window until the job is done.

saying these in an interview costs you the question

  • Says the client opens a GET stream to receive server messages
  • Claims MCP needs separate endpoints for JSON and for streaming
  • Sends Accept: application/json only, then wonders why nothing streams
  • Thinks the client chooses whether the reply is streamed
  • Expects a JSON-RPC response body back from a notification

context

open as a page

What headers must an MCP Streamable HTTP POST carry, and what is HeaderMismatchError?

level: middleimportance: must knowfreq 62%

basics

~10 s

An MCP Streamable HTTP request must carry MCP-Protocol-Version, Mcp-Method, and Mcp-Name for tools/call, resources/read and prompts/get. If MCP-Protocol-Version disagrees with the value in params._meta, the server returns HTTP 400 with JSON-RPC error -32020, HeaderMismatchError.

open as a page

Why does an MCP Streamable HTTP server answer GET or DELETE with 405 Method Not Allowed?

level: middleimportance: should knowfreq 58%

basics

~20 s

Because in MCP revision 2026-07-28 the Streamable HTTP endpoint is POST-only. The GET listening stream and the DELETE termination call both belonged to earlier revisions, so a server implementing only 2026-07-28 SHOULD reject those verbs with 405 Method Not Allowed.

open as a page

In MCP Streamable HTTP, what does closing the response stream mean to the server?

level: seniorimportance: should knowfreq 48%

basics

~20 s

It means cancellation. In MCP revision 2026-07-28 a server MUST treat the client closing the HTTP response stream as cancellation of that request and stop the work. Anything that drops the connection — a proxy idle timeout, a closed tab — therefore cancels the call.

open as a page

Why must an MCP Streamable HTTP server validate the Origin header and return 403?

level: seniorimportance: should knowfreq 42%

basics

~20 s

Because a web page can make the browser POST to any reachable HTTP endpoint, including one on localhost. MCP revision 2026-07-28 requires the server to validate the Origin header and answer an invalid one with 403, so a hostile page cannot drive a locally running MCP server's tools.

open as a page