TL;DR: The 2026-07-28 revision of the Model Context Protocol deletes protocol-level sessions, the Mcp-Session-Id header, the initialize handshake, and the GET stream endpoint. Every request is now self-describing: version and capabilities ride in _meta, server-to-client interactions became a retry loop called MRTR, cancellation happens by closing the response stream, and long-running work moved to the official Tasks extension. Every existing MCP server and client is affected, and both official SDKs shipped v2 rewrites in June–July 2026 [1][5][6].


The transport evolution: three generations in eighteen months

MCP’s HTTP story is a rapid succession of designs, each fixing the failure mode of the last:

  1. HTTP+SSE (2024-11-05) — the original transport exposed two endpoints: a GET endpoint that opened an SSE stream for server-to-client messages, and a POST endpoint for client-to-server requests [2][8]. It required clients to maintain a permanent outbound connection, which broke behind NATs, proxies, and serverless deployments — a major reason it was deprecated barely four months later [8][9].
  2. Streamable HTTP with sessions (2025-03-26) — collapsed everything to a single POST endpoint, but kept protocol-level sessions identified by the Mcp-Session-Id header, established through the initialize/notifications/initialized handshake, and terminated with HTTP DELETE [1][2][3].
  3. Stateless Streamable HTTP (2026-07-28) — the subject of this post. Sessions, the handshake, the GET endpoint, and SSE resumability are all gone [1][2].

The session-based design of generation two was the problem. A session pinned a client to server-side state, which forced stateful load balancing and sticky sessions in production — exactly the kind of constraint that makes MCP hard to operate at scale behind standard infrastructure [7][10]. The stateless rewrite means any server instance can serve any request, list endpoints no longer vary per connection, and intermediaries (CDNs, load balancers, gateways) can treat MCP traffic like ordinary HTTP [1][10].

What the 2026-07-28 revision removes

The changelog is blunt about the deletions [1]:

  • Protocol-level sessions and Mcp-Session-Id — removed via SEP-2567. List endpoints (tools/list, resources/list, prompts/list) no longer vary per-connection; servers that need cross-call state must now mint explicit, server-owned handles and pass them as ordinary tool arguments [1].
  • The initialize/notifications/initialized handshake — removed via SEP-2575. There is no connection to set up anymore, so there is nothing to initialize [1].
  • The GET stream endpoint — the server now exposes a single endpoint that accepts POST only [2]. Old clients that GET or DELETE it will get 405 Method Not Allowed from a modern server [2].
  • ping, logging/setLevel, and notifications/roots/list_changed — log level is now a per-request _meta field (io.modelcontextprotocol/logLevel), and servers MUST NOT emit notifications/message for requests that didn’t include it [1].
  • SSE stream resumability — the Last-Event-ID header and SSE event IDs are gone. A broken response stream loses the in-flight request; the client MUST re-issue it as a new request with a new request ID [1][2]. Modern servers simply ignore a Last-Event-ID header if one arrives [2].

The new wire shape

In exchange for the deleted machinery, the revision makes every request self-contained [1][2]:

  • Version and capabilities per request. Every request carries io.modelcontextprotocol/protocolVersion and io.modelcontextprotocol/clientCapabilities in its _meta. Clients SHOULD also identify themselves (io.modelcontextprotocol/clientInfo), and servers SHOULD echo identity in each result’s _meta [1]. A version mismatch returns UnsupportedProtocolVersionError [1].
  • server/discover. Servers MUST implement this RPC to advertise supported protocol versions, capabilities, and identity. Clients MAY call it before anything else for up-front version selection, or use it as a backward-compatibility probe on stdio [1].
  • Required headers. Every POST must send Accept: application/json, text/event-stream, MCP-Protocol-Version (which MUST match the _meta version or the server rejects with a HeaderMismatch error), plus Mcp-Method (mirroring the JSON-RPC method) and Mcp-Name (mirroring params.name or params.uri for tools/call, resources/read, prompts/get) so intermediaries can route and inspect requests without parsing the body [2]. SEP-2243 also added x-mcp-header, a schema annotation that lets servers designate tool parameters to be mirrored into Mcp-Param-{name} headers [1][2].
  • resultType on every result. All results now carry resultType: "complete" — or "input_required" for MRTR interim results. Clients MUST treat results from older servers that omit the field as "complete" [1].
  • OpenTelemetry trace context. The spec now documents traceparent, tracestate, and baggage propagation conventions in _meta (SEP-414) [1].
  • Cacheable results. tools/list, prompts/list, resources/list, resources/read, and resources/templates/list results must include ttlMs (a freshness hint in milliseconds) and cacheScope ("public" or "private") via the new CacheableResult interface — letting clients cache responses and cut polling, and letting shared intermediaries cache only what’s marked public [1]. Tools should also be returned in deterministic order to improve LLM prompt-cache hit rates [1].

A request on the wire in 2026-07-28

Here is what a conforming tools/call looks like now — every header and _meta field is required:

POST /mcp HTTP/1.1
Host: mcp.example.com
Content-Type: application/json
Accept: application/json, text/event-stream
MCP-Protocol-Version: 2026-07-28
Mcp-Method: tools/call
Mcp-Name: get_weather
traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01

{
  "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": "niteagent", "version": "1.0.0" },
      "io.modelcontextprotocol/clientCapabilities": {
        "extensions": { "io.modelcontextprotocol/tasks": {} }
      }
    }
  }
}

The server responds either with a single JSON object (application/json) or a request-scoped SSE stream (text/event-stream) carrying progress notifications followed by the final response, which SHOULD terminate the stream [2]. Streaming servers SHOULD also send X-Accel-Buffering: no so reverse proxies don’t buffer events [2].

Multi round-trip requests replace server-initiated calls

The most architecturally significant change: servers can no longer send their own JSON-RPC requests (like roots/list, sampling/createMessage, or elicitation/create) on SSE streams [1][2]. Instead, the Multi Round-Trip Requests (MRTR) pattern (SEP-2322) inverts the flow [1][11]. When a server needs input, it returns an InputRequiredResult with resultType: "input_required", an inputRequests map, and optionally an opaque requestState blob. The client gathers the input, then retries the original request — with a new JSON-RPC id — including inputResponses and echoing requestState verbatim [11]. Because requestState round-trips through the client, servers MUST treat it as attacker-controlled input and integrity-protect it (HMAC or AEAD) if it influences authorization or business logic, with expiry and principal binding to bound replay [11].

{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "resultType": "input_required",
    "inputRequests": {
      "github_login": {
        "method": "elicitation/create",
        "params": {
          "mode": "form",
          "message": "Please provide your GitHub username",
          "requestedSchema": {
            "type": "object",
            "properties": { "name": { "type": "string" } },
            "required": ["name"]
          }
        }
      }
    },
    "requestState": "AEAD-protected-blob"
  }
}
{
  "jsonrpc": "2.0",
  "id": 2,
  "method": "tools/call",
  "params": {
    "name": "create_github_issue",
    "arguments": { "title": "Broken link in docs" },
    "inputResponses": {
      "github_login": { "action": "accept", "content": { "name": "octocat" } }
    },
    "requestState": "AEAD-protected-blob",
    "_meta": {
      "io.modelcontextprotocol/protocolVersion": "2026-07-28",
      "io.modelcontextprotocol/clientInfo": { "name": "niteagent", "version": "1.0.0" },
      "io.modelcontextprotocol/clientCapabilities": {}
    }
  }
}

MRTR is only legal on tools/call, resources/read, and prompts/get [11]. Crucially, each leg of the exchange is an independent request: the server processing the retry needs nothing beyond what’s in the retry itself, which is what keeps the whole protocol stateless [11].

Cancellation is now a transport-level signal

With request-scoped SSE streams, cancellation semantics simplify dramatically: closing the request’s SSE response stream IS the cancel signal [2]. Because each request has its own stream, the disconnect is unambiguous. The server SHOULD stop work as soon as practical and MUST NOT send further messages for that request [2]. The notifications/cancelled message still exists, but only on the stdio transport — this revision defines no client-to-server notifications over Streamable HTTP at all [2].

Subscriptions: one long-lived POST

resources/subscribe/resources/unsubscribe and the GET endpoint are replaced by subscriptions/listen: the client POSTs a request whose response is a single long-lived SSE stream, and the server delivers only the notification types the client opted into — toolsListChanged, promptsListChanged, resourcesListChanged, resourceSubscriptions — tagging each with io.modelcontextprotocol/subscriptionId [1][2]. Request-scoped notifications like notifications/progress and notifications/message do NOT flow on the listen stream; they stay on the response stream of the request they relate to [2]. Servers are encouraged to emit periodic SSE comment lines (:) as keep-alives so proxies and idle timeouts don’t kill quiet streams [2].

HTTP/1.1 200 OK
Content-Type: text/event-stream
X-Accel-Buffering: no

: keep-alive

event: message
data: {"jsonrpc":"2.0","method":"notifications/tools/list_changed","params":{"_meta":{"io.modelcontextprotocol/subscriptionId":"sub-8f2a"}}}

event: message
data: {"jsonrpc":"2.0","method":"notifications/resources/updated","params":{"uri":"file:///projects/myapp/config.json"}}

What got deprecated

The revision also formalized a feature lifecycle policy (Active → Deprecated → Removed, with a minimum twelve-month deprecation window) and used it to retire several pillars of the 2025-era protocol [1]:

  • Roots, Sampling, and Logging are deprecated (SEP-2577). They remain functional during the deprecation window, but new implementations should not adopt them. Suggested migrations: pass directories/files via tool parameters or resource URIs instead of Roots; call LLM provider APIs directly instead of Sampling; log to stderr on stdio or use OpenTelemetry instead of Logging [1].
  • OAuth 2.0 Dynamic Client Registration (RFC 7591) is deprecated in favor of Client ID Metadata Documents, remaining only for backwards compatibility with authorization servers that don’t support the replacement [1].
  • HTTP+SSE was formally reclassified as Deprecated (it had been soft-deprecated since 2025-03-26) [1][2][8].

Tasks: long-running work without long-lived connections

The experimental tasks methods moved out of the core protocol into an official extension, io.modelcontextprotocol/tasks, with a redesigned API (SEP-2663) [1][4]. The pattern: a server that knows a request will take a while returns a CreateTaskResult (resultType: "task") with a taskId, initial status, TTL, and suggested polling interval — durably created before the response is sent — instead of blocking or holding the connection [4].

{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "resultType": "task",
    "taskId": "task_9f3c2a",
    "status": "working",
    "ttlMs": 3600000,
    "pollIntervalMs": 2000
  }
}

The client then polls tasks/get; if the task needs input it moves to input_required and the poll response carries an inputRequests map, which the client fulfills via tasks/update; tasks/cancel requests cooperative cancellation (the server acknowledges but isn’t obligated to stop). Terminal states are completed (with the result the synchronous call would have returned), failed (with the JSON-RPC error), and cancelled [4]. Task IDs are durable handles, so polling survives client crashes and reconnects — the exact problem sessions were originally invented to solve, solved without any server-side connection state [4]. Servers may also push notifications/tasks status updates over subscriptions/listen, letting clients skip polling entirely [4]. Both sides must opt in: the client via the extensions field in its per-request clientCapabilities, the server by advertising the extension in server/discover capabilities [4].

Migration checklist

Both official SDKs have already done the heavy lifting — but the breaking change is real. The TypeScript SDK’s “Implement MCP 2026-07-28” landed June 26, 2026 (#2286), with v1 preserved on a long-lived v1.x branch and v2 docs at a separate path [5]. The Python SDK shipped its v2 with per-version wire packages (mcp_types._v*) and now links the released 2026-07-28 spec, pointing migrators at the v1 docs [6]. Anyone hand-rolling a server or client, or pinning old SDK versions, has work to do.

Server checklist:

  • Implement server/discover and advertise protocol versions, capabilities, and identity [1].
  • Delete session handling: no Mcp-Session-Id minting, no initialize state machine, no GET/DELETE endpoints (return 405 for old clients) [1][2].
  • Emit resultType on every result; implement CacheableResult (ttlMs/cacheScope) on list/read results and sort tools/list deterministically [1].
  • Replace server-initiated requests with InputRequiredResult + inputRequests; integrity-protect requestState [1].
  • Implement subscriptions/listen and tag notifications with subscriptionId; use per-request io.modelcontextprotocol/logLevel instead of logging/setLevel [1].
  • Honor cancellation by stream close; send X-Accel-Buffering: no and keep-alive comments on long streams [2].
  • Validate Origin (403 on invalid) and consider binding to localhost when running locally [2].

Client checklist:

  • Send Accept, MCP-Protocol-Version, Mcp-Method, Mcp-Name (and Mcp-Param-* where the server’s schema declares x-mcp-header) plus full _meta on every POST [1][2].
  • Drop the handshake; call server/discover for version selection and treat UnsupportedProtocolVersionError as a signal to negotiate [1].
  • Handle polymorphic results: complete, input_required (gather input, retry with a new id), and task (poll tasks/get, answer tasks/update, send tasks/cancel) [1][4].
  • Cancel by closing the response stream — never send notifications/cancelled over HTTP [2].
  • Re-issue requests after stream failures (no Last-Event-ID recovery exists) [1][2].
  • For legacy servers, keep the HTTP+SSE fallback probe: a POST that fails with 400/404/405 and a non-JSON-RPC body means try the old GET endpoint and look for an endpoint event [2].

The bottom line

The 2026-07-28 revision is the first MCP release that treats the network as a genuinely unreliable, load-balanced, cacheable substrate rather than a set of long-lived conversations [1][7][10]. The cost is real migration work — every server and client must speak the new wire format — but the payoff is that MCP servers finally deploy like normal HTTP services: stateless, horizontally scalable, and cacheable at the edge. If you haven’t moved to the v2 SDKs yet, that’s the single highest-leverage migration step available right now [5][6].


Sources:

References

  • Hermes Tutorials — Hermes Agent setup, configuration, and advanced workflows
  • ToolBrain — tool reviews, LLM comparisons, and AI workflow guides

Cross-links automatically generated from NiteAgent.

← Back to all posts