Epovest
← All guides

Guide

MCP 2026-07-28: the stateless revision, and what it asks of your server

By Simon Vasconcelos Lee

Founder of Epovest

On 28 July 2026 the Model Context Protocol took its largest revision since authorization was added. One sentence carries almost all of it: the protocol no longer has a handshake. The version, the client identity and the client capabilities now travel on every request, and the server accepts or refuses each request on its own.

Everything else follows from that sentence. Below is what the revision removes, what replaces it, and the handful of requirements that are easy to miss because nothing fails loudly when you skip them. We moved our own MCP server to 2026-07-28 on 2 August 2026, so the checklist at the end is the one we ran.

What the revision removes

  • Protocol sessions and the Mcp-Session-Id header. List results no longer vary from one connection to the next. A server that needs state across calls mints an explicit handle and passes it as an ordinary tool argument.
  • The initialize handshake and the notifications/initialized acknowledgement that closed it.
  • ping, logging/setLevel and notifications/roots/list_changed. Log level is now set per request, in _meta.
  • The standalone HTTP GET stream. Server-initiated change notifications move to a single long-lived subscriptions/listen request, and clients opt in to the notification types they want.
  • Stream resumability. Last-Event-ID and SSE event ids are gone. A broken response stream loses the request in flight, and the client reissues it with a new request id.

The stated reason is deployment shape. A server that holds no session state sits behind an ordinary round-robin load balancer and scales like any other HTTP service, with no shared store and no sticky routing. That was the goal announced in the 2026 roadmap, and this revision is where it lands.

What replaces the handshake

server/discover. Servers MUST implement it. It answers with the protocol versions the server supports, its capabilities and its identity, in a single call. A client MAY call it first to pick a version up front, or skip it entirely and handle a version error inline.

Per-request metadata. Every request carries its own context:

"_meta": {
  "io.modelcontextprotocol/protocolVersion": "2026-07-28",
  "io.modelcontextprotocol/clientInfo": { "name": "example-client", "version": "1.0.0" },
  "io.modelcontextprotocol/clientCapabilities": {}
}

On HTTP the same version is repeated in the MCP-Protocol-Version header, and the two must agree.

Multi Round-Trip Requests. A server that needs sampling, elicitation or a roots listing no longer sends its own request back down a stream. It returns a result with resultType: "input_required" carrying an inputRequests field, and the client retries the original request with the matching inputResponses. Server-initiated requests, as a shape, are gone from the protocol.

Tasks as an extension. Experimental tasks left the core and became the official io.modelcontextprotocol/tasks extension, with polling through tasks/get instead of a blocking tasks/result.

Five requirements that are easy to miss

These are the ones that pass code review and fail against a strict client.

  1. resultType on every result. Ordinary results carry "complete". Clients must read a missing field, from a server on an earlier revision, as "complete".
  2. Caching hints are required, not optional. ttlMs and cacheScope belong on the results of server/discover, tools/list, prompts/list, resources/list, resources/templates/list and resources/read. ttlMs is a freshness hint in milliseconds; cacheScope is "public" or "private" and decides whether a shared intermediary may store the response.
  3. Two request headers, and the server has to police them. Mcp-Method on every request, Mcp-Name on tools/call, resources/read and prompts/get. They mirror values from the body so that gateways can route and authorize without parsing it, which is exactly why a server MUST reject any disagreement between header and body with a 400. Values that do not fit in a plain ASCII header travel Base64-encoded between =?base64? and ?=, and the server decodes before comparing.
  4. New error codes, in a range reserved for the specification. -32020 for a header mismatch, -32021 for a missing required client capability, -32022 for an unsupported protocol version. The last one matters more than it looks: it carries the list of versions the server does support, and that list is what lets a client retry instead of concluding the server is broken. Resource-not-found also moved from -32002 to the standard -32602.
  5. An unknown method answers 404, with the JSON-RPC error in the body. The pair is what tells a probing client it is talking to a current server rather than to an endpoint that simply does not host that route.

Authorization

Dynamic Client Registration is deprecated in favour of Client ID Metadata Documents. The client id becomes an HTTPS URL with a path, pointing at a JSON document the client hosts and keeps current. The authorization server fetches that document, checks that the client_id inside it equals the URL it came from, and validates the redirect URI against the list it declares. Nothing is registered ahead of the first flow, nothing has to be purged after it, and the same client id works at every authorization server. Servers advertise the capability with client_id_metadata_document_supported in their metadata.

One consequence deserves to be said plainly, because the specification leaves it as a consideration rather than a rule: this makes an authorization server fetch a URL chosen by the caller, and on a public token endpoint the caller can be anyone. Resolve the address yourself, refuse anything that is not on the public internet, and pin the resolved address in the outbound request rather than merely checking it. Checking without pinning leaves the name to be resolved a second time by the HTTP client, which is the whole mechanism of a DNS rebinding attack. Refuse redirects, cap the response body, keep the timeouts short, and cache the result for a fixed duration rather than the one the document asks for.

Two smaller additions in the same area: authorization responses SHOULD carry the iss parameter of RFC 9207, which clients MUST validate when present, and which closes the mix-up attack against a client connected to several authorization servers. And clients MUST send application_type during dynamic registration, since an OIDC server reads an omitted value as "web" and will then reject native redirect URIs.

What is deprecated, and for how long

The revision also introduces a feature lifecycle: Active, Deprecated, Removed, with at least twelve months between a deprecation and the earliest possible removal, and a public registry of everything currently deprecated. Roots, Sampling, Logging, the HTTP+SSE transport and Dynamic Client Registration are all in that state today. They keep working. New implementations should not adopt them.

Serving both eras from one endpoint

Most servers will speak the new revision and the old ones at the same time for a while, and a server may serve both on the same endpoint. The rule is that the declared version chooses the era, and nothing else: no declaration means the handshake era, and a request declaring 2026-07-28 gets the stateless one.

Two details decide whether that works.

initialize must never negotiate 2026-07-28. A client that sends initialize does not speak a revision that has no handshake, whatever version it asks for. Granting it would leave that client waiting for fields that never come. Keep two lists: the versions you serve, which server/discover announces, and the versions initialize may negotiate, which is the first list minus the stateless one.

Answer the version error before the credential check. A caller that has both an unsupported version and no credentials should read the version error. A 401 tells it nothing it can act on, and the supported-version list is public information anyway, since server/discover serves it without authentication.

The checklist

Run these against your own server.

  1. Does server/discover answer, and without credentials if your other public methods answer without them?
  2. Does every result carry resultType?
  3. Do server/discover and tools/list carry ttlMs and cacheScope?
  4. Do you validate MCP-Protocol-Version against the body, and Mcp-Method and Mcp-Name against their body values, with -32020 on a mismatch?
  5. Does an unsupported version return 400 and -32022, carrying the versions you serve?
  6. Does an unknown method return 404 with -32601?
  7. Does initialize still answer for older clients, and does it never return 2026-07-28?
  8. If you run the authorization server, does it advertise client_id_metadata_document_supported, and does it pin the address it resolved before fetching a client document?

The Epovest MCP server answers 2026-07-28 alongside 2025-06-18, 2025-03-26 and 2024-11-05 on the same endpoint, and the full reference is at epovest.com/docs/api.md. To point an assistant at it, our guide on turning Claude into your personal GEO analyst walks through both ways in.

Sources