The failure
A remote server stored client capabilities after initialize and issued an Mcp-Session-Id. Requests were fine until the load balancer sent the next POST to another process. That process knew the session ID but not the session.
Adding sticky routing would hide the symptom. It would not produce a 2026-07-28 server.
Build a separate stateless path first
Keep the legacy endpoint behavior while introducing a request path that accepts the new revision. Do not delete fallback during the first deployment. Your test matrix needs one pinned 2026 client and one older client.
MCP-Protocol-Version: 2026-07-28 MCP-Method: tools/list
The body now carries the context the second replica was missing:
"_meta": {
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
"io.modelcontextprotocol/clientInfo": {
"name": "catalog-client",
"version": "2.1.0"
},
"io.modelcontextprotocol/clientCapabilities": {}
}Add discovery, but do not turn it into initialize
Implement server/discover with supportedVersions and server capabilities. The result tells a client what the endpoint supports. It does not create state that later requests depend on.
A useful test: restart the server between discovery and tools/list. The second request should still succeed.
Reject ambiguity
If the HTTP version and body version disagree, return the standard header-mismatch error instead of choosing one. Reject an unsupported version clearly. Authenticate every request; clientInfo is a label, not identity.
Run the trace
Paste the headers and two or three representative JSON-RPC messages into MCP 2026 Stateless Check. A clean report only proves the visible envelope is coherent. Finish with integration tests that restart processes, switch replicas and exercise the legacy fallback.
Expected result
A pinned 2026 client makes requests without initialize or a session ID. An older client deliberately negotiates the old path. Either path survives routing to a fresh process.
Validate your migration trace →