Since its introduction in late 2024, the Model Context Protocol has seen rapid, steady adoption. As the protocol evolved in response to requests from its users, version 2026-07-28 introduced substantial changes.
Earlier articles in this series covered what MCP is and how to build a server in .NET (both in Italian). Here we look at what changed and why.
From stateful to stateless
The biggest change is the move from stateful to stateless operation. All the other changes, large and small, follow from it.
MCP was originally designed mainly to connect local processes through a pipe, where client and server can each write whenever they want. HTTP does not have that symmetry: the client speaks and the server replies. To enable server-to-client communication anyway, the protocol introduced a persistent session, identified by an Mcp-Session-Id, through which the server could send messages as needed.
This has two costs. First, you have to maintain state. Second, and more troublesome, a client request becomes tied to the particular server instance that opened the session. That makes scaling harder: the load balancer must keep routing a client to the same instance, and the session dies if that instance fails.
The new revision changes this flow to fit HTTP better and, above all, to improve scalability and reliability.
MRTR: Multi Round-Trip Requests
If the server needs information from the client, it simply responds: its response asks for what it needs and ends the exchange. Once the client has gathered the information, it calls the server again with the same request, supplemented by the answer.
Consider a concrete example. The close_ticket tool closes a support ticket, but first asks the user to confirm the reason. This is the smallest case that needs an interaction in the middle of a call, and exactly where the two revisions diverge.
In round 1, the client calls tools/call. The server does not have the closing reason, so it cannot finish the job. It responds with resultType: input_required, a question for the user, and a requestState. The HTTP call ends here: the server releases the connection, the handler finishes, and nothing remains in memory. The requestState is the only continuity between the two rounds. The server produces it, and the client sends it back unchanged.
In round 2, the client repeats the same tools/call, adding the received requestState and the user’s answer in inputResponses. Now the server has everything it needs and finishes with resultType: complete.
The field to watch is resultType: input_required means “I need something else; call me again,” while complete means the exchange is over.
You can see the difference at a glance in the traces of the very same tool, run first with a session and then with MRTR.

With a session: 26 spans, depth 8. You can see initialize, elicitation/create nested inside tools/call, and the final DELETE that tears down the session. In between, a GET / lasts 0.15 seconds on a 0.37-second trace: that is the server-to-client channel, open for the whole session.

With MRTR: 19 spans, depth 4. The two tools/call calls are siblings, not one nested inside the other. There is no GET bar and no DELETE.
The change in depth makes the shift in approach visible: one call used to contain a conversation; now there are two independent calls.
Goodbye handshake
As part of the move to stateless operation, the initialization mechanism is gone too: the initialize/notifications/initialized exchange and Mcp-Session-Id give way to standalone requests.
The handshake did two jobs, and those jobs have taken different paths.
Client information — protocol version, capabilities, and identity — told the server whom it was talking to. It now travels with every request inside _meta, under three keys:
io.modelcontextprotocol/protocolVersion
io.modelcontextprotocol/clientCapabilities
io.modelcontextprotocol/clientInfo
This is, quite literally, the content of initialize repeated in every request. These fields are mandatory: if one is missing, the server rejects the request.
{"error":{"code":-32602,"message":
"Requests using protocol version '2026-07-28' must include
'_meta/io.modelcontextprotocol/protocolVersion'."}}
Server information — serverInfo and serverCapabilities — let the client learn what the server could do. This is now an ordinary, optional server/discover request: a client that already knows which tool to call can skip it and go straight to tools/call.
In other words, a negotiation in which both sides agreed and remembered the result has become a declaration repeated in every request.
👉 A practical tip: with the .NET SDK, a GET request to a stateless server endpoint returns 405 Method Not Allowed, because GET and DELETE routes are not even registered in that mode. The specification does not guarantee this behavior, but it is a quick external clue to how a server is running.
The other changes
- Header-based routing.
Mcp-MethodandMcp-Namehave been introduced. Since requests all use POST to the same endpoint, a proxy previously had to read and parse the JSON body to route them. Now two headers are enough. - Cacheable lists. Responses from
tools/list,prompts/list,resources/list, andresources/readnow carryttlMsandcacheScope, telling the client how long and within what scope to cache them. Without a session to depend on, caching becomes possible. - Roots and Sampling deprecated. These were requests initiated by the server toward the client. They will keep working for at least twelve months, but new implementations should avoid adopting them.
- Logging deprecated. Logging was a notification, not a question awaiting an answer, so it cannot even use MRTR. OpenTelemetry is the solution here; if you want to explore it, see the dedicated series on this blog.
- Elicitation remains. It has not been deprecated: only its transport changes, and it becomes the main use case for MRTR.
- Authorization. There are changes here too, but they deserve an article of their own.
Pros, cons, and what to watch for
As with any change, there are trade-offs and details that need attention.
The system scales more easily, but extra round trips add latency: an interaction that used to fit in one call now needs two. On a local network that may mean milliseconds; on a slow connection, much more.
Most of all, we need to pay attention to how we write the server, because idempotency now needs to be handled.
Conclusion
MCP has moved from “the server can call back to the client in the middle of a request” to “the server replies and forgets about you.” The end of sessions, new headers, cacheable lists, and deprecated features all follow from that decision.
The price is a few more round trips and the responsibility of handling idempotency. In return, a server can sit behind an ordinary load balancer without special configuration and restart without drastic effects on ongoing conversations.
The next article gets practical: how to write a .NET server for the new revision and migrate older servers (in Italian). Simply upgrading the package compiles without a warning, then breaks at runtime.