The Model Context Protocol published a significant specification update on July 28, 2026. The headline change: sessions are gone. There’s no more initialize/initialized handshake, no Mcp-Session-Id header, no server-initiated requests over standing SSE streams. Every MCP request is now self-contained.
This is a breaking change for existing servers, but the motivation is clean and the migration path is well-defined. This post breaks down what changed technically, why it matters for deployment, and how to adapt existing implementations.
The Problem Sessions Solved (and Created)
In every previous MCP spec from 2025-03-26 through 2025-11-25, clients opened a connection to an MCP server with an initialize exchange. The server returned its capabilities and the client sent initialized to confirm. A session ID tied all subsequent requests to that specific server instance.
This worked fine for single-server deployments. It became a problem at scale.
Remote MCP servers behind load balancers needed sticky sessions — every request from a given client had to route to the same backend instance, because only that instance had the session context. No session context meant no capability information, no version negotiation result, no record of what the client could do. If that instance died, the client’s session died with it.
Sticky sessions are an operational burden. They limit horizontal scaling, complicate zero-downtime deployments, and require shared storage for any failover. They’re also incompatible with the kind of stateless, disposable infrastructure that cloud-native deployments assume.
The 2026-07-28 spec solves this at the protocol level: embed everything the server needs into every request.
The New Request Format
Every modern MCP request (method: 2026-07-28 or later) includes these fields in the _meta parameter:
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "get_weather",
"arguments": { "location": "Seattle, WA" },
"_meta": {
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
"io.modelcontextprotocol/clientInfo": {
"name": "ExampleClient",
"version": "1.0.0"
},
"io.modelcontextprotocol/clientCapabilities": {}
}
}
}
Two fields are required on every request:
io.modelcontextprotocol/protocolVersion— the protocol version for this specific requestio.modelcontextprotocol/clientCapabilities— what this client can do
clientInfo is strongly recommended but not required. Servers must reject requests missing required fields with -32602 Invalid params and, on HTTP, 400 Bad Request.
Every result response also includes a new required resultType field: "complete" for finished requests, "input_required" for the new multi-round-trip pattern described below. Clients must treat absent resultType as "complete" for backward compatibility with older servers.
The server/discover RPC is now required on all servers. It takes only _meta and returns supportedVersions[], capabilities, and server info — clients can call it first to probe what the server supports without risking a full request.
New HTTP Headers for Infrastructure Routing
The spec adds three new HTTP headers that mirror request metadata into the HTTP layer:
Mcp-Protocol-Version— the protocol versionMcp-Method— the JSON-RPC method nameMcp-Name— the name of the tool/prompt/resource being called
These headers exist so load balancers, WAFs, gateways, and rate limiters can route and inspect requests without parsing the JSON body. A gateway can apply different rate limits to tools/call for expensive_tool versus fast_tool purely from headers, with no body inspection.
Servers must reject requests where headers don’t match the body — mismatch returns error code -32020 HeaderMismatch.
Tool definitions can also annotate parameters with x-mcp-header to have specific argument values mirrored into Mcp-Param-{Name} headers. This enables tenant-based or region-based routing at the infrastructure layer without body parsing.
Multi Round-Trip Requests (MRTR): Replacing Server Push
In previous specs, servers could send requests to clients by pushing JSON-RPC messages over a standing SSE stream. This enabled patterns like sampling (ask the client to run an LLM call), elicitation (collect user input), and roots (ask for filesystem directories). All of these required an open connection for the server to push on.
The stateless spec breaks that model. There’s no standing connection to push on.
MRTR is the replacement. When a server needs information from the client mid-request, instead of pushing a message, it responds to the current request with resultType: "input_required" and an inputRequests map:
{
"resultType": "input_required",
"inputRequests": {
"user_confirmation": {
"method": "elicitation/create",
"params": {
"mode": "form",
"message": "Confirm deployment target",
"requestedSchema": {
"type": "object",
"properties": { "confirmed": { "type": "boolean" } },
"required": ["confirmed"]
}
}
}
},
"requestState": "AEAD-protected-blob"
}
The original request terminates. The client collects the requested information, then retries the original call with a different id (important: they’re independent requests) plus inputResponses and the echoed requestState. The server decodes requestState to reconstruct whatever context it needs and completes the operation.
The requestState field is the mechanism for state continuity. Servers encode whatever they need (serialized as a base64 blob, JWT, AEAD-protected payload) and clients echo it back verbatim. Clients must treat it as opaque — no parsing, no modification. Because it passes through an untrusted client, servers must protect its integrity with HMAC or AEAD if it influences authorization decisions. The spec recommends including the authenticated principal, a short TTL, and the original request identifier inside the integrity-protected payload to prevent replay.
MRTR is supported on prompts/get, resources/read, and tools/call. It is the mandatory replacement for all previous server-to-client push patterns.
What’s Deprecated
The spec formally deprecates four features, with removal targeting no earlier than July 2027:
Roots — the mechanism for servers to ask clients for filesystem root directories. Migration: pass directories via tool parameters, resource URIs, or server configuration. In the interim, use MRTR elicitation/create inside inputRequests.
Sampling — the mechanism for servers to ask clients to run LLM calls on their behalf. Migration: call LLM provider APIs directly from the server. In the interim, sampling/createMessage is still supported as a request type inside MRTR’s inputRequests, even though Sampling as a standalone client capability is deprecated.
Logging — previously sent log messages to clients via the push channel. Migration: log to stderr for stdio transport; use OpenTelemetry for observability. The spec notes OTel fields (traceparent, tracestate, baggage) are now reserved _meta keys following W3C Trace Context standard.
Dynamic Client Registration — replaced by Client ID Metadata Documents (CIMD). This one is authorization-layer plumbing that primarily affects OAuth flows.
The GET stream endpoint on Streamable HTTP (where clients could open a standing SSE channel for server-initiated messages) is also removed. Servers must respond 405 Method Not Allowed to GET requests.
Tasks: Async Long-Running Operations
The Tasks extension (io.modelcontextprotocol/tasks) addresses a problem MRTR doesn’t solve: what happens when an operation takes longer than the transport will wait?
Long-running tool calls have always been awkward. You can’t hold an HTTP connection open for minutes without hitting infrastructure timeouts. The previous pattern was often “just return fast and let the client poll” — but without a standardized mechanism.
Tasks formalizes this. When a server decides an operation will be long-running, it returns resultType: "task" with a CreateTaskResult containing a taskId, initial status, ttlMs, and pollIntervalMs. The client polls tasks/get { taskId } to check status.
Task states: working → input_required (if mid-task input is needed, via a similar inputRequests mechanism) → completed or failed or cancelled. When completed, the result field contains what the original synchronous call would have returned.
Task IDs are durable — clients that disconnect and restart can resume polling. The spec advises clients to persist task IDs for exactly this reason.
Notifications are optional: servers can push status updates via notifications/tasks through a subscriptions/listen stream, but polling is the default pattern. This gives servers the option to be push-capable without requiring it.
Migration for Existing Servers
The spec defines explicit backward compatibility detection:
Dual-era servers on the same endpoint — serve both modern and legacy on the same URL. If the request has modern _meta fields, serve statelessly. If it’s an initialize request, enter legacy mode.
HTTP detection — a modern client attempts a modern POST. If it gets 400 Bad Request and the body is a recognized modern JSON-RPC error, the server is modern and the request was malformed. If the 400 body is something else, fall back to initialize.
stdio detection — client sends server/discover first. If it gets a DiscoverResult or UnsupportedProtocolVersionError, it’s modern. Anything else → fall back to initialize.
The practical migration order:
- Add
_metaparsing to your request handler — extract version and capabilities - Add
server/discoverimplementation - Add
resultTypeto all responses - Implement MRTR for any patterns currently using server-initiated requests (sampling, roots, elicitation)
- Implement the Tasks extension for any long-running operations
- Remove session tracking once all clients support modern spec
SDKs: TypeScript, Python, Go, and C# are Tier 1 and updated. Rust is in beta.
What This Changes for AI Agent Infrastructure
The architecture implications extend beyond MCP server implementation:
Load balancer configuration simplifies. Remove sticky session rules for MCP backends. Round-robin across all instances works correctly. This unlocks simpler auto-scaling and eliminates the failure mode where a load balancer routes to a dead instance that holds the client’s session.
Multi-tenant deployments become cleaner. Each request carries its own identity; there’s no session state to leak across tenants if an instance is reused. This reduces the blast radius of session isolation failures.
Gateway capabilities expand. The new HTTP headers enable routing, rate limiting, and access control at the infrastructure layer without application changes. Expensive tool calls can be rate-limited by name at the WAF before the request reaches your server.
Observability improves. With OTel fields as first-class _meta keys, distributed traces can now span from the MCP client through the server to downstream LLM calls without custom instrumentation glue.
The stateless transition is a breaking change for existing server implementations, but it’s the right call at the infrastructure level. The previous session model was a mismatch for how modern cloud deployments work. The 2026-07-28 spec aligns MCP with the operational patterns that the rest of distributed systems infrastructure has converged on over the last decade.
Thuận Lương is a Technical Lead with 15+ years of experience in .NET, cloud architecture, and AI systems. He writes about lessons learned building real production systems.