Streamable HTTP
One endpoint, POST only
Section titled “One endpoint, POST only”In Streamable HTTP the server is an independent process that can serve many clients at once. It exposes a single HTTP endpoint path — the MCP endpoint, e.g. https://example.com/mcp — that accepts POST. At a glance:
- The client sends every JSON-RPC request or notification as its own HTTP POST.
- The server answers a request with either a single JSON object or a Server-Sent Events stream scoped to that one request.
- Server-to-client interactions (elicitation, sampling, roots) are embedded in results as
InputRequiredResult, never sent as separate requests. - Long-lived change notifications arrive on the response stream of a
subscriptions/listenrequest.
Streamable HTTP replaced the HTTP+SSE transport in 2025-03-26; revision 2026-07-28 removed the GET stream and protocol-level sessions. If you have seen Mcp-Session-Id or a GET endpoint, that is legacy (2025-11-25 and earlier) — see Backward Compatibility.
sequenceDiagram participant C as Client participant S as Server Note over C,S: Simple response C->>S: POST tools/call S-->>C: 200 application/json (response) Note over C,S: Streaming response C->>S: POST tools/call S-->>C: 200 text/event-stream: notifications/progress S-->>C: SSE: notifications/progress S-->>C: SSE: response, stream closes Note over C,S: Change notifications C->>S: POST subscriptions/listen S-->>C: SSE: notifications/subscriptions/acknowledged S-->>C: SSE: notifications/tools/list_changed (stream stays open)
Sending
Section titled “Sending”Rules for the client on every POST:
- Use
POST. - Send
Accept: application/json, text/event-stream— the client must support both response shapes. - Include the request metadata headers (below).
- The body is a single JSON-RPC request or notification. Clients never POST JSON-RPC responses.
If the body is a notification and the server accepts it, it replies 202 Accepted with no body; otherwise an HTTP error such as 400. If the body is a request, the server replies with Content-Type: application/json (one object) or Content-Type: text/event-stream (an SSE stream). The server chooses per request; the client must handle either.
Receiving an SSE stream
Section titled “Receiving an SSE stream”When the server picks SSE, the stream is scoped to the originating request:
- It may carry notifications that relate to that request —
notifications/progress,notifications/message— before the final response. - It must not carry independent JSON-RPC requests. (Legacy servers could push
sampling/createMessageon SSE; modern ones cannot.) - The final JSON-RPC response should terminate the stream.
Two operational rules for servers: send X-Accel-Buffering: no so reverse proxies like nginx do not buffer events, and on long streams emit an SSE comment line (: followed by a newline) periodically as a keep-alive — clients must ignore comment lines. Resumable streams via Last-Event-ID are not supported; a broken response stream loses the in-flight request and the client re-issues it with a new ID.
The request metadata headers
Section titled “The request metadata headers”Streamable HTTP mirrors selected body fields into headers so intermediaries can route without parsing JSON.
| Header | Source | Required for |
|---|---|---|
MCP-Protocol-Version | _meta["io.modelcontextprotocol/protocolVersion"] | All requests |
Mcp-Method | method | All requests |
Mcp-Name | params.name or params.uri | tools/call, resources/read, prompts/get |
POST /mcp HTTP/1.1Content-Type: application/jsonAccept: application/json, text/event-streamMCP-Protocol-Version: 2026-07-28Mcp-Method: tools/callMcp-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": {} } }}For resources/read, Mcp-Name carries the URI, e.g. Mcp-Name: file:///projects/myapp/config.json.
Server validation
Section titled “Server validation”A server that processes the body must check that the headers match it. Any mismatch, missing required header, or invalid character is rejected with 400 Bad Request and JSON-RPC error -32020 (HeaderMismatch):
{ "jsonrpc": "2.0", "id": 1, "error": { "code": -32020, "message": "Header mismatch: Mcp-Name header value 'foo' does not match body value 'bar'" }}Why so strict? A load balancer might route on the header while the server executes on the body. If they can disagree, an attacker can aim a request at one tenant’s route and another tenant’s data. Header names are case-insensitive; values are case-sensitive.
Two more status codes to know: an unsupported protocol version → 400 with UnsupportedProtocolVersionError (-32022) listing supported versions; an unknown RPC method → 404 Not Found with -32601. The JSON-RPC body is what distinguishes a modern 404 from a legacy server that simply has no /mcp route.
Custom headers from tool parameters
Section titled “Custom headers from tool parameters”A server may mark a tool parameter with x-mcp-header in its inputSchema; conforming HTTP clients then mirror that argument into Mcp-Param-{Name}:
{ "name": "execute_sql", "inputSchema": { "type": "object", "properties": { "region": { "type": "string", "x-mcp-header": "Region" }, "query": { "type": "string" } }, "required": ["region", "query"] }}A call with "region": "us-west1" carries Mcp-Param-Region: us-west1, so a gateway can route to the right region before the body is ever parsed. Constraints: only primitive string/integer/boolean parameters (not number), names must be valid HTTP tokens and unique, and the property must be reachable purely through properties keys — never through items, oneOf, $ref and so on. A client must drop a tool whose annotations break these rules rather than send it.
Values that are not plain ASCII, or that have leading/trailing whitespace or newlines, are Base64-encoded with a sentinel, and the same rule covers an unusual Mcp-Name:
Mcp-Param-Greeting: =?base64?SGVsbG8sIOS4lueVjA==?=Never mark passwords, API keys, tokens or PII with x-mcp-header. Header values are visible to every intermediary on the path.
Long-lived notifications
Section titled “Long-lived notifications”There is no GET stream. A client that wants change notifications sends a subscriptions/listen POST; the server’s response is itself an SSE stream that stays open and carries only the types the client opted into (toolsListChanged, promptsListChanged, resourcesListChanged, resourceSubscriptions). Request-scoped notifications such as progress never travel on that stream — they stay on the response stream of the request they belong to. Details in change notifications.
Cancellation
Section titled “Cancellation”Closing the SSE response stream must be treated by the server as cancellation of that request. Each request has its own stream, so the disconnect is unambiguous; no notifications/cancelled message is expected over HTTP. The server should stop work promptly and must not send more messages for it.
Security requirements
Section titled “Security requirements”- Servers must validate the
Originheader on all incoming connections to prevent DNS rebinding. A present-but-invalidOrigingets403 Forbidden. - When running locally, bind to
127.0.0.1, not0.0.0.0. - Implement proper authentication for all connections — for remote servers that means the OAuth 2.1 flow in Security and Authorization.
Without these, a web page in the user’s browser could talk to a local MCP server as if it were the host.