The Stateless MCP Revision: What 2026-07-28 Changes for Authorization and Audit

Protocol revision 2026-07-28 is the largest change to MCP since authorization was added. It removes protocol-level sessions, removes the initialize handshake, and moves server-to-client requests into a retry pattern. If your server was written against 2025-11-25 or earlier, most of the work is transport plumbing. A smaller part of it is security work, and that part is what a customer's reviewer will ask about.

This is a migration note for teams running a server in production: what changed, what it does to identity and audit, and what to test before you ship the upgrade.

Versions are now declared per request

There is no handshake. Every request carries its protocol version in _meta under io.modelcontextprotocol/protocolVersion, and over Streamable HTTP the same value appears in the MCP-Protocol-Version header. The server accepts or rejects each request independently. If it does not support the requested version it returns UnsupportedProtocolVersionError (code -32022) listing the versions it does support, and the client retries.

Servers must also implement server/discover, an RPC that returns supported versions, capabilities, and self-reported server identity. Clients may call it first, or may call any method directly and handle the version error.

Two consequences for the server you already run:

  • Version is now untrusted input on every call. Validate the _meta value and the header, reject a mismatch, and make sure a client cannot select an older revision to reach a code path where a control is weaker. If you keep legacy support, the old path needs the same audience validation, the same scope checks, and the same audit events as the new one.
  • server/discover is a public surface unless you make it otherwise. Decide deliberately whether it answers before authorization, and keep its response free of tenant-specific detail. The spec notes that serverInfo is self-reported and must not drive security decisions; the same applies in reverse, so do not use a client's self-reported clientInfo for authorization either. Client identity comes from the token and the registered client, not from a name in _meta.

Sessions are gone, so state becomes an object you must authorize

Mcp-Session-Id is removed. List endpoints no longer vary per connection. Where a server previously kept cross-call state in a session, the pattern now is an explicit, server-minted handle passed back as an ordinary tool argument.

That is a clean design, and it moves a trust boundary. A handle travels through the model's context, so it can be logged, copied, or injected into another conversation. Treat every handle as a bearer reference:

  • Mint it server-side with unguessable entropy. Never derive it from a record ID.
  • Bind it at mint time to the user, the tenant, and the client, and check that binding on every use against the identity in the current token. A handle presented by a different subject is a denial and an audit event, not a cache miss.
  • Give it a short expiry and a use count where the operation allows one.
  • Keep authorization decisions attached to the underlying resource, not to the handle. A handle is a pointer, not a grant.

If you were relying on the session to hold anything security-relevant — a cached entitlement, a tenant selection, a completed confirmation step — that state now has to be re-derived or re-checked per request. Re-checking per request is the correct outcome, and it removes a class of stale-permission bug, but it is a real change to your data access layer.

Audit correlation loses its free identifier

The session ID was doing quiet work in a lot of audit schemas: it was the thing that tied a run of tool calls together. Without it, a naive upgrade produces audit events that no longer group.

Replace it before you ship, not after an incident:

  • Keep a per-request correlation ID as you already do.
  • Propagate the caller's trace context. The revision documents OpenTelemetry conventions for traceparent, tracestate, and baggage in _meta, which gives you a grouping key that survives the hop.
  • Where a handle exists, record it (or its hash) on each event so a sequence of calls against the same object is reconstructable.
  • State the change in your evidence pack. Reviewers who saw the previous schema will notice a field disappear.

Logging itself moved too: logging/setLevel is removed, log level is set per request in _meta, and the Logging feature is deprecated in favour of stderr or OpenTelemetry. Your operational logging is not your audit trail and never was; this is a good moment to make sure the two are separate sinks with different retention.

Server-initiated requests become retries

Sampling, elicitation, and roots are no longer server-initiated calls. Under Multi Round-Trip Requests, the server returns a result with resultType: "input_required" carrying inputRequests, and the client retries the original request with inputResponses. Results now always carry resultType, and results from older servers that omit it are treated as "complete".

The security point is that the retry is a new request. It arrives with its own token, and nothing in the protocol guarantees it is the same principal, the same client, or even the same person at the keyboard. So:

  • Re-authenticate and re-authorize the retry exactly as you would a first call. Do not resume from stored state on the strength of a correlation identifier alone.
  • Carry any server-side correlation in requestState, with the same binding rules as a handle. The elicitationId field and the notifications/elicitation/complete notification are removed, so a server that needs to match an out-of-band interaction across retries mints its own identifier and must validate it.
  • Keep the confirmation semantics you had. If a destructive tool required a human decision, that decision must still be recorded as its own audit event and must still be unforgeable by the model.

Stream resumability is also gone: Last-Event-ID and SSE event IDs are removed, and a broken stream loses the in-flight request, which the client re-issues with a new request ID. Anything with a side effect needs an idempotency key, or your users will get double writes on flaky networks.

Authorization details worth a line in the upgrade ticket

The authorization model is unchanged in shape — OAuth 2.1, protected resource metadata, resource indicators, audience validation — but three details moved:

  • Dynamic Client Registration (RFC 7591) is deprecated in favour of Client ID Metadata Documents. It still works for authorization servers that do not support CIMD. If your registration policy was built around DCR, plan the move rather than discovering it at a directory re-submission.
  • Authorization servers should return the iss parameter (RFC 9207), and clients must validate it before redeeming the code. If you operate the authorization server, turn it on.
  • Clients must key persisted credentials by issuer and re-register when the authorization server changes. If you run more than one issuer across environments, this closes a real cross-environment credential reuse hole.

The HTTP+SSE transport is now formally deprecated under the feature lifecycle policy, as are Roots, Sampling, and Logging. Deprecated features stay in the specification for at least twelve months, so nothing breaks next week. It does mean that new work should not adopt them, and that a reviewer reading your architecture document will ask why they are there.

A test plan for the upgrade

Before the migration goes to a customer, run it:

  1. Requests declaring an unsupported version, a missing version, and mismatched header and _meta values.
  2. A legacy-era client against the dual-era path, checking that audience validation, scope enforcement, and audit emission are identical on both paths.
  3. A handle minted for tenant A replayed by tenant B, by a different user in tenant A, and after expiry.
  4. An MRTR retry presented with a different token, an expired token, and a requestState value the server did not mint.
  5. A dropped response stream mid-write, repeated, to confirm idempotency.
  6. server/discover unauthenticated, checking the response for anything tenant-specific.

Then diff your audit sample against the previous one and update the evidence pack. A version upgrade that changes the shape of your audit events is a change the customer's security team is entitled to hear about from you first.

Where this lands

Most teams will do this migration once, in a hurry, because a client SDK forced it. The parts that get skipped in a hurry are handle binding, retry re-authorization, and the audit fields that quietly disappeared — which is to say, exactly the parts a security review examines.

Tracking the specification and writing the impact note is part of our ongoing support work, and a migration is a sensible trigger for an MCP security audit if nobody outside the team has read the server since it was built.