On 28 July 2026 the Model Context Protocol published a new revision of its specification. The changelog opens with two items that change how every remote server works: protocol-level sessions are gone, and so is the initialize handshake. Each request now carries everything the server needs to answer it.
For many servers that is a migration. For a server that reads identity documents it is closer to a confirmation, because that kind of server had good reasons to be stateless already. This post goes through the revision from the point of view of a tool server: which changes it can ignore, which ones it has to build, and which ones are worth adopting early. The examples come from a server we run, so the gaps named below are our own as well.
This is the blog of doc.cheap, a Passport and ID OCR API with an MCP server in front of it. Read the product parts with that in mind.
The short version
| Change in 2026-07-28 | What a stateless tool server does |
|---|---|
No Mcp-Session-Id, no sessions |
Nothing, if it never kept one |
No initialize handshake; version and capabilities in each request's _meta |
Read them per request, check the MCP-Protocol-Version header against them |
server/discover |
Implement it: it is a MUST |
Mcp-Method and Mcp-Name headers required |
Check that they match the body |
resultType on every result |
Send "complete" |
ttlMs and cacheScope on list and read results |
Pick a cache lifetime and a scope |
| Tools in a deterministic order | Keep the registration order stable |
| HTTP GET stream and resumability removed | Answer GET and DELETE with 405 |
| Roots, Sampling and Logging deprecated | Take files as tool arguments, log to stderr or OpenTelemetry |
The rest of the post explains each row and why it matters more for a server that handles documents.
Sessions are gone, and that suits document servers
The first item of the changelog removes "protocol-level sessions and the Mcp-Session-Id header from the Streamable HTTP transport". List endpoints such as tools/list "no longer vary per-connection", and servers that need state across calls "use explicit, server-minted handles passed as ordinary tool arguments".
A document server rarely wants a session in the first place. Think about what a session would hold: an API key, maybe an image waiting to be read, maybe the last result. All of that is data you would rather not keep in a process's memory between calls, where a bug can hand it to the next caller.
Our hosted server at https://mcp.doc.cheap/mcp was built that way before this revision. The code says it is "stateless on purpose": every POST gets a fresh server and a fresh transport, answers and is thrown away, so "no session lives in memory, a restart loses nothing, and one caller's key can never be seen by another caller's request". The transport is created without a session id generator, which in the TypeScript SDK is what makes it stateless, and answers with plain JSON rather than a stream.
The handle pattern the revision describes is also already there. When an agent wants a stored result back, it does not rely on a session to remember it. scan_document returns a scan id, and the get_scan and delete_scan tools take that id as an ordinary scan_id argument. The id is the handle. Any server instance can answer it, and so can a client that reconnects tomorrow.
If your server keeps something per session, the question to ask is the same one: can it become an id the server returns and the client passes back? For document work the answer is usually yes, and usually a privacy gain.
No handshake: the version travels with every request
The second item makes MCP stateless at the protocol level. The initialize and notifications/initialized exchange is removed, and "every request now carries its protocol version and client capabilities in _meta". On HTTP the version also goes in a header. The Streamable HTTP page says every POST "MUST include an MCP-Protocol-Version header", and that it must match the value in _meta; if not, the server answers 400 with a HeaderMismatch error.
Here is what a call to a document tool looks like in the new shape. The headers and the _meta keys follow the specification's own example; the tool and its arguments are ours, with placeholders:
POST /mcp HTTP/1.1
Content-Type: application/json
MCP-Protocol-Version: 2026-07-28
Mcp-Method: tools/call
Mcp-Name: scan_document
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "scan_document",
"arguments": { "image_base64": "<base64 image>", "retain_hours": 0 },
"_meta": {
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
"io.modelcontextprotocol/clientInfo": { "name": "<client>", "version": "<version>" },
"io.modelcontextprotocol/clientCapabilities": {}
}
}
}
For the server this means three checks on every request instead of one handshake:
- The version. If the server does not support the version asked for, it answers 400 with an
UnsupportedProtocolVersionErrorthat lists the versions it does support. - The headers against the body.
Mcp-Methodis required on every request, andMcp-Nameontools/call,resources/readandprompts/get. They mirror the method and the tool name, and a mismatch is aHeaderMismatcherror. - The capabilities. Whatever the server used to remember from
initializeit now reads from the request in hand.
The headers matter more than they look for a document server. A scan_document call carries a whole image as base64; our hosted server accepts request bodies up to 36 MiB. With Mcp-Method and Mcp-Name in the headers, a gateway can route, count or rate-limit the expensive tool apart from the cheap ones without reading that body. The specification adds one warning for such gateways: they "SHOULD verify" that the version header names a version that requires header and body validation, and reject the request otherwise, rather than trust a header nobody checked.
server/discover is required
The third item adds server/discover, and "servers MUST implement this RPC to advertise their supported protocol versions, capabilities, and identity". Clients may call it first to pick a version, or use it as a compatibility probe on stdio.
This is the one change a stateless server cannot skip. Our server still exposes a small discovery document on GET / (name, version, transport and the endpoint), but that is a plain web page, not the protocol method. A server that wants to speak 2026-07-28 answers server/discover itself.
Results: resultType, caching and order
Three smaller items change what a result looks like.
resultType. Every result now carries it:"complete"for an ordinary answer,"input_required"for the interim answer of a multi round-trip request. Clients "MUST treat results from earlier-protocol servers that omit the field as"complete"", so an older server keeps working, but a new one sends it.ttlMsandcacheScope. Results oftools/list,prompts/list,resources/list,resources/readandresources/templates/listmust carry a freshness hint in milliseconds and a scope,"public"or"private". For a server whose tool list only changes on deploy and whose resources are bundled documentation, a longttlMswith"public"is a natural fit. A resource that depends on the caller's key would be"private".- Deterministic tool order. Servers "SHOULD return tools from
tools/listin a deterministic order", to help client caching and prompt cache hits. If tools are registered in code in a fixed order, keep it that way; ours registers six in the same order every time.
Asking the user for more: multi round-trip requests
Older revisions let a server send its own requests to the client in the middle of a call: elicitation/create to ask the user something, sampling/createMessage to borrow the client's model, roots/list to learn the allowed folders. The revision replaces that with the Multi Round-Trip Requests pattern. The server answers with resultType: "input_required" and a list of inputRequests, and the client retries the original request with inputResponses.
For a document tool this is the clean place for a question such as "the photo is cut off on the left, send another one?". Because the retry is the same request again, nothing has to be held on the server between the two calls. A server that used none of the server-initiated requests, as ours did not, has nothing to migrate here.
What is deprecated, and what to use instead
Roots, Sampling and Logging are deprecated. They keep working during a deprecation window of at least twelve months, but "new implementations should not add support for them". The suggested replacements fit a document server well:
- Instead of Roots, pass files as tool arguments or configuration. Our local server reads a file path only when an image directory is set in its configuration, and only inside that directory; a path outside it is refused. The model chooses the argument; the operator chooses the fence.
- Instead of Logging, log to stderr or OpenTelemetry. The revision also removes
logging/setLevel; a log level now travels per request in_meta, and servers must not emit log messages for requests that did not ask. Trace context (traceparent) can ride in_metatoo.
Old clients will keep calling
Clients do not all move on the same day, so the transport page describes what a server that supports only 2026-07-28 should do with older traffic:
- GET or DELETE to the MCP endpoint: 405 Method Not Allowed.
- An
Mcp-Session-Idheader: ignore it, and never mint or echo one. - A
Last-Event-IDheader: ignore it, because streams are not resumable any more.
Our hosted server already answers GET and DELETE on its endpoint with 405 and an Allow: POST, OPTIONS header, and mints no session id. A client that still speaks an older revision finds out from the error body: the specification tells it to fall back to initialize only when the 400 body is not a recognised modern error.
Where our server stands
To be exact about it: the hosted server is built on version 1.30.0 of the TypeScript SDK, and our own code does not name 2026-07-28 anywhere yet. The parts above that are already true for it are the ones that come from its design: no sessions, ids as handles, 405 for GET and DELETE, files fenced by configuration. server/discover, the header checks, resultType and the cache fields are the work that is left. The passport OCR MCP server page and the MCP server guide describe what clients connect to today.
If you are choosing a document API for agents, the passport OCR API comparison lists the services; the MCP side is worth asking each one about separately.
A checklist for your own tool server
- No state lives between requests; anything a later call needs is an id the server returns.
- Each request's version is checked against the
MCP-Protocol-Versionheader, withUnsupportedProtocolVersionErrorfor a version you do not support. Mcp-MethodandMcp-Nameare checked against the body.server/discoveranswers with versions, capabilities and identity.- Results carry
resultType; list and read results carryttlMsandcacheScope. tools/listreturns the same order every time.- GET and DELETE get 405; session and event-id headers are ignored.
- Files come in as arguments inside a configured fence, not through Roots.
- Logs go to stderr or OpenTelemetry, not to the client.
The full text is on modelcontextprotocol.io, and the release post gives the background. More about our server is on the passport OCR MCP server page.
A question for you: did your MCP server keep anything in a session that you now have to turn into a handle? Tell us in the comments below.