Enterprise systems accumulate trust in the places where state lives. A session identifier
is not just a routing key — it is the thing a dozen downstream assumptions quietly hang
from. Remove it and you do not remove one field. You invalidate every assumption that was
resting on it, including the ones nobody wrote down.
The Model Context Protocol's 2026-07-28 specification did exactly that. The project called
it "the largest revision of the protocol since launch",
and the two headline changes are both acts of subtraction. From the
changelog's major
changes, items 1 and 2:
Remove protocol-level sessions and the
Mcp-Session-Idheader from the Streamable HTTP
transport.Make MCP stateless: remove the
initialize/notifications/initializedhandshake. Every
request now carries its protocol version and client capabilities in_meta.
At the protocol layer, every request now stands on its own. Authorization gets stricter
alongside it: clients
MUST validate a present iss parameter against the recorded issuer per
RFC 9207 before redeeming an
authorization code, which closes a class of authorization-server mix-up attacks. There is
a formal extensions framework, and a minimum twelve-month deprecation window before any
deprecated feature can be removed.
That is a good specification. This post is not about the specification.
What early tracking actually costs
I had MCP tests running against the stateless profile on 2026-07-22, six days before
the final specification was published. I want to be precise about what that bought and what
it cost, because the first part is the part people write posts about and the second part is
the part that matters.
Between the release candidate and the final revision, the spec added an error-code
allocation policy. The JSON-RPC server-error range got partitioned: -32000 to -32019
stays implementation-defined and grandfathered, -32020 to -32099 is reserved for the
specification. Then it renumbered the codes the draft had introduced:
| Error | Release candidate | Final |
|---|---|---|
HeaderMismatch |
-32001 |
-32020 |
MissingRequiredClientCapability |
-32003 |
-32021 |
UnsupportedProtocolVersion |
-32004 |
-32022 |
My harness checked for -32004.
The function that reads that code decides one thing: when a server rejects the modern
protocol version, is that an explicit stateless-protocol answer, or is it an old server
that does not understand the request? Get it right and the probe stops. Get it wrong and
the client falls back to initialize — the handshake the specification just removed.
Against any server built to the final spec, my check returned false. The harness read a
compliant version rejection as evidence of a legacy server, sent the removed handshake,
and reported results for a protocol the server had explicitly refused.
The function's own docstring said that must not happen. It happened anyway.
By the time the final specification landed, that assumption was already on PyPI.
Version 4.10.0 shipped on 2026-07-25, carrying the RC comparison verbatim. You do not
have to take my word for it: download the wheel and read protocol_tests/mcp_harness.py.
The RC comparison is in the published artifact, and -32022 appears nowhere in it.
The test suite stayed green the entire time
This is the part worth sitting with.
I have unit tests covering that exact function. They assert the fallback does not fire on
a version rejection. They passed continuously — before the spec was final, after it was
final, and through the release that carried the defect.
They passed because every fixture in them was written during the RC window and pinned
-32004. The tests and the code were wrong in the same direction, so they agreed with
each other perfectly.
A test that exercises only the provisional RC value cannot detect that the released
value is unhandled. Its branch coverage may be adequate, but its oracle is not independent: the
implementation and the fixture are two copies of the same provisional assumption. They can
agree perfectly and still be wrong.
I did not find this by testing. I found it by re-reading the specification changelog
against my own code, line by line, while checking something else.
The cost of tracking a specification early is inheriting the decisions it had not finished making
Being six days ahead of the final specification is a real advantage and I would do it
again. But a release candidate is a set of provisional commitments, and tracking one means
adopting those commitments before they have been tested by the people who will have to live
with them. Some of them will change. The changes will be small, unglamorous, and exactly the
kind your fixtures will freeze in place.
The lead is not free. It is a loan against a spec that has not stopped moving.
What to check, concretely
If you maintain anything that touches MCP and you moved during the RC window, four things
are worth an hour:
Grep for hardcoded JSON-RPC error codes. Any literal in -32000..-32099 that you
wrote between the RC and 2026-07-28 is suspect. Move them to named constants — a named
constant makes a renumber a deliberate edit instead of a silent one. But keep the test
oracle independent: assert the released wire value directly, or derive the fixture from an
authoritative conformance vector — not from the production constant being tested. A test
that imports the constant it is checking is tautological, and that is the same failure in
a tidier shape.
Check what your fixtures pin, not just what your tests assert. If every fixture for a
behaviour was authored in the same week, they encode that week's assumptions and will
agree with each other forever. Add one fixture from the current standard and see what
breaks.
Audit anything that decides "modern or legacy." Version-negotiation branches fail
quietly by design — they are written to degrade gracefully, which means a wrong answer
produces a plausible run instead of an error.
Re-read the changelog after the final specification, not just at RC. Diff it against your own
code rather than your memory of the RC. The renumbering that caught me is item 12 under
"Minor changes." Nothing about its placement suggests it breaks a client.
Where this leaves the testing layer
The stateless rewrite reset a meaningful part of MCP security testing. For implementations
targeting 2026-07-28, protocol-level session assumptions are gone, per-request capability
declaration is new surface, and authorization requirements have tightened. Tests scoped to
earlier MCP revisions may remain valid, but they are not evidence of conformance to the new
one. Suites written against the old handshake are not slightly stale — they are asserting
things about a mechanism that no longer exists in the 2026-07-28 protocol profile.
That is an opening for anyone willing to re-derive their assumptions from the current
text. It is also a trap for anyone who moved early and has not gone back.
I moved early. I went back. It cost me one released assumption that became a
compatibility defect three days later — and an afternoon to find it. I would rather publish
that than the version where I only mention the six-day lead.
The fix, the negative controls, and the reasoning are public: PR #313.
Views are my own.
Top comments (0)