The Model Context Protocol shipped its 2026-07-28 revision today, the version most people are already calling MCP v2. It is the largest change to the protocol since it launched, and unlike previous revisions it contains deliberate breaking changes.
If you run a remote MCP server, one change matters far more than the rest: sessions are gone. Everything else in the release either follows from that or is additive.
This post covers what changed from MCP v1, which parts actually break, how clients negotiate between the two, and what we changed in Server4Agent.
Quick answer
MCP v2 (the 2026-07-28 spec) makes the protocol stateless. The initialize handshake and the Mcp-Session-Id header are removed, so any server instance can answer any request without sticky sessions or a shared session store. Capabilities move to a server/discover call, protocol metadata rides in _meta on every request, list responses become cacheable, and long-running work gets a formal Tasks extension. Clients on v2 still connect to v1 servers through a documented fallback, so existing servers do not stop working overnight.
Key takeaways
- The headline change is a stateless core: no handshake, no session header, no sticky routing.
server/discoverreplacesinitialize, and per-request_metareplaces the one-time capability exchange.- List results carry
ttlMsandcacheScope, so clients cache instead of holding a stream open to watch for changes. - Extensions became a formal framework, with MCP Apps and a redesigned Tasks extension as the first official ones.
- Roots, sampling, and logging are deprecated with a twelve-month minimum window before removal.
- Authorization tightened around OAuth mix-up defences, and that part applies to your server whether or not you adopt v2.
- Client SDKs default to legacy negotiation today, so a v1 server keeps working. The pressure to migrate is real but not immediate.
The one change that matters: MCP went stateless
In MCP v1, a connection had a lifecycle. The client sent initialize, the server answered with its protocol version and capabilities, the client sent notifications/initialized, and from then on both sides shared state that had been established once. Over HTTP, that state was pinned with an Mcp-Session-Id header.
That design has a cost that only shows up in production. A session-bound server needs sticky sessions at the load balancer, or a shared session store every instance can reach, or both. Scaling it horizontally means making that state available everywhere, and a gateway that wants to route intelligently has to open the request body to find out what the request is.
MCP v2 removes the concept. There is no handshake and no session identifier. Every request is self-describing: the protocol version, client identity, and client capabilities travel in the _meta field of each request, under keys like io.modelcontextprotocol/protocolVersion and io.modelcontextprotocol/clientInfo.
The practical consequence is that a remote MCP server can now sit behind a plain round-robin load balancer with nothing shared between instances.
MCP v1 vs MCP v2 at a glance
| Area | MCP v1 (through 2025-11-25) | MCP v2 (2026-07-28) |
|---|---|---|
| Connection setup | initialize + notifications/initialized handshake | No handshake; every request is self-describing |
| Session identity | Mcp-Session-Id header, sticky routing | Removed entirely |
| Capability discovery | Returned once from initialize | server/discover, which servers must implement |
| Protocol metadata | Exchanged once at connection setup | In _meta on every request |
| List freshness | Hold a stream open, wait for listChanged | ttlMs + cacheScope on list results |
| Server-to-client requests | SSE stream held open | Multi round-trip InputRequiredResult + requestState |
| Long-running work | Ad-hoc: return a handle, client polls your custom tool | Tasks extension: tasks/get, tasks/update, tasks/cancel |
| Tool schemas | Constrained JSON Schema subset | Full JSON Schema 2020-12, including composition |
| Structured results | structuredContent limited to objects | structuredContent accepts any JSON value |
| Missing-resource error | -32002 (MCP-specific) | -32602 Invalid Params (JSON-RPC standard) |
| Roots, sampling, logging | Active | Deprecated, minimum twelve-month window |
| Extensions | Informal, experimental features in core | Formal framework with reverse-DNS IDs |
What replaces the handshake
Servers on v2 must implement server/discover. It returns the supported protocol versions, capabilities, and server identity: the same information initialize used to return, but callable at any time by any instance rather than once per connection.
Clients use it two ways: as an up-front version selection step, and as a probe to work out whether a server is modern at all.
The rest of the metadata moves into the request envelope. Where v1 established "who I am and what I support" once, v2 restates it on every call:
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "create_project",
"arguments": { "name": "invoice-parser" },
"_meta": {
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
"io.modelcontextprotocol/clientInfo": { "name": "example-host", "version": "1.0.0" }
}
}
}That looks redundant until you remember the point: the instance that answers this request may never have seen the client before, and does not need to have.
Requests now route on headers
Three headers govern HTTP transport in v2. MCP-Protocol-Version carries the revision, Mcp-Method names the operation, and Mcp-Name names the tool, resource, or prompt being addressed.
The reason is infrastructure, not protocol elegance. A gateway can route, rate-limit, or authorize on Mcp-Method: tools/call without parsing a JSON-RPC body. Servers are required to reject requests where the headers and the body disagree, so the headers can be trusted by things sitting in front of the server.
Caching replaces the notification treadmill
In v1, a client that wanted to know when your tool list changed had to keep a stream open and wait for a listChanged notification. That is a lot of held connections to answer a question that, for most servers, has the same answer all day.
v2 puts caching directives on list and resource-read results instead. ttlMs says how long the response stays fresh, and cacheScope says whether it can be shared across users. It mirrors ordinary HTTP Cache-Control semantics, and clients cap the values; the TypeScript client, for instance, refuses to honour a TTL beyond 24 hours.
If your tool list is a constant that changes only when you deploy, this is close to free traffic reduction. If your tool list varies by user or plan, cacheScope is the field that keeps you correct: get it wrong and you will serve one tenant's tool list to another.
Tasks: long-running work gets a real shape
Anyone who has built an MCP server that does slow work has invented the same pattern: return a job handle, then expose a custom get_status tool and hope the agent remembers to poll it.
v2 promotes that into the Tasks extension. A server answers tools/call with a task handle, and the client drives execution with tasks/get, tasks/update, and tasks/cancel. Task creation is server-directed, so the server decides what deserves a task rather than the client guessing. Note that tasks/list was removed during the redesign: tasks are driven by handle, not enumerated.
The gain is that hosts can render progress natively instead of every server teaching every agent its own polling convention.
MCP Apps: servers can ship interface
The other official extension lets servers ship interactive HTML rendered in a sandboxed iframe on the host. Tools declare their UI templates ahead of time so hosts can prefetch them and security-review them, and actions taken in that UI route back through the same JSON-RPC protocol as a direct tool call.
It is opt-in, and it is the clearest signal of where the extensions framework is headed: capabilities ship as versioned extensions with reverse-DNS identifiers and their own maintainers, rather than accumulating in the core spec.
Authorization got stricter, and this part is not optional
Six separate proposals tightened MCP's OAuth profile. The one most likely to affect you today: clients must now validate the iss parameter on authorization responses, per RFC 9207, to prevent mix-up attacks between authorization servers.
Here is the trap. That validation lives in the client's auth layer, not its protocol handler. A host that upgrades its SDK starts validating iss against your authorization server regardless of which protocol version it negotiates with you. If your authorization endpoint does not return iss, strict clients can reject the response, and your v1 server, which you had not planned to touch, stops accepting new OAuth connections.
Dynamic client registration also gained application_type, so clients declare whether they are native or web rather than falling into an incorrect default, and credentials are now explicitly bound to the issuing authorization server.
What v2 deprecates
Three capabilities entered the deprecation lifecycle:
- Roots: replaced by tool parameters, resource URIs, or server configuration.
- Sampling: servers that want model output should call a model provider directly.
- Logging: use stderr on stdio transports, or OpenTelemetry for structured observability.
All three keep working for at least twelve months, and removal requires its own proposal. If you never advertised them, this section costs you nothing.
Alongside them, v2 added a formal feature lifecycle (active, deprecated, removed, with minimum twelve-month windows between stages) specifically so a release like this one does not happen again without warning.
Does MCP v2 break your v1 server?
This is the question that actually matters, and the answer is more reassuring than "breaking changes" suggests.
We tested it rather than guessed. Pointing the published MCP client SDK at a server that implements only the v1 methods (answering initialize, returning -32601 Unknown method for anything else, exactly as a v1 server does) produces this:
| Client negotiation mode | Result | What went over the wire |
|---|---|---|
| Default (no options) | Connects | initialize → tools/list |
legacy | Connects | initialize → tools/list |
auto | Connects | server/discover → falls back → initialize → tools/list |
Pinned to 2026-07-28 | Fails | server/discover, then a negotiation error |
Two findings worth internalising.
First, the fallback is real. In auto mode the client probes server/discover, reads a plain -32601 as "this server is legacy", and transparently drops to the initialize path. v2 distinguishes that from its own typed negotiation errors (unsupported protocol version, header mismatch, missing required client capability), so "modern server that rejected your request" and "old server that has never heard of this method" are no longer confused.
Second, and more usefully: the shipped client SDK defaults to legacy negotiation. A host has to explicitly opt into auto, or pin a version, to send server/discover at all.
So you break only if a host pins 2026-07-28, a deliberate choice that also forfeits every other pre-v2 server, or when a future SDK release flips that default. Neither is something you can see from your side, which is the real argument for not waiting until you can.
How Server4Agent handles MCP v2
We will be straight about what is shipped and what is not, because "supports MCP v2" is a claim that is easy to make and easy to check.
The stateless core cost us nothing. Our MCP endpoint has always been a single stateless HTTP handler. No Mcp-Session-Id, no session store, no SSE stream, no sticky routing; every request already carried everything needed to answer it. The change that forces most remote MCP servers into a migration is, for us, an architecture we already had. Multiple projects sharing a server box, each with its own workspace and URL, never depended on connection state.
We shipped the authorization hardening. Our authorization endpoint now returns iss on every response that leaves through the redirect URI, the authorization code and every error alike, and advertises authorization_response_iss_parameter_supported in its metadata. As explained above, that one is not really optional, so it went out first. Our OAuth flow already used PKCE with dynamic client registration, protected-resource metadata, and issuer-and-audience-bound tokens.
We corrected our transport semantics. An authenticated client opening the optional server-to-client stream now gets a 405 with Allow: POST, which is the accurate "we do not offer that here", instead of a 401 that a strict client could misread as a rejected token. Unauthenticated requests still receive the 401 challenge that makes OAuth discovery work.
We answer `server/discover`. It returns every revision we speak, 2024-11-05 through 2026-07-28, along with our capabilities and identity, and any instance can answer it at any time. A client negotiating in automatic mode now completes a v2 handshake with us in a single call, and one that pins 2026-07-28 connects rather than failing.
We speak both wire formats, per request. Because v2 clients declare their version on every call, in the _meta envelope or the MCP-Protocol-Version header, we decide the response format per request rather than per connection. A v2 caller gets the resultType envelope, structuredContent alongside the text block, and ttlMs / cacheScope on tool lists. A pre-2026 caller gets none of those fields, because they would reject them. We also reject any request whose routing headers disagree with its body (the Mcp-Method, the Mcp-Name of the tool being called, or the declared revision itself), so a gateway routing on those headers cannot be steered somewhere the body did not ask for.
Tool results come back as data. Every tool that returned a JSON string stuffed into a text block now also returns it as structuredContent, so a v2 host hands your agent typed data instead of a string to re-parse. The text block stays for older clients and for models that read it directly.
What is not shipped: the Tasks extension. Our long-running operations (builds, deploys, exec) still return a handle the agent polls, rather than a task handle driven by tasks/get and tasks/cancel. Tasks moved out of the core specification into a separate versioned extension in this release, and the published client SDK ships no runtime for it, so there is nothing yet to test an implementation against. We would rather wait for that than guess at the contract.
Every claim above was verified by running the published MCP client SDK against our endpoint in each of its negotiation modes: default, legacy, automatic, and pinned to 2026-07-28.
If you are connecting an assistant today, none of this requires anything from you: point your MCP-compatible host at our endpoint with an API key or OAuth, and the negotiation is handled.
What to do now, if you run an MCP server
- Return `iss` on your authorization responses. This is the only item that can break existing users without them adopting v2 at all. Do it first.
- Check what your `GET` handler returns to an authenticated client. If you do not offer a notification stream, the answer is
405, not401. - Implement `server/discover` alongside `initialize`. Keep both. The legacy handshake is how every client that has not upgraded still reaches you.
- Audit your session assumptions. If you have sticky routing or a shared session store, that is the migration. If you do not, you are most of the way there already.
- Decide your `cacheScope` before you set `ttlMs`. A tool list that varies per user must not be cached across users.
- Leave roots, sampling, and logging alone. They have at least twelve months, and their replacements are more direct anyway.
FAQ
Is "MCP v2" the same thing as the 2026-07-28 spec?
Yes. MCP revisions are dated rather than numbered, so the official name is the 2026-07-28 specification. "v2" is shorthand that stuck because this is the first revision with intentional breaking changes.
Does MCP v2 break my existing v1 server?
Not by itself. Client SDKs default to legacy negotiation, and in automatic mode they probe server/discover and fall back to initialize when a server does not implement it. You break if a client pins the new version, or if a future SDK changes its default.
What happened to `Mcp-Session-Id`?
Removed. Protocol-level sessions are gone entirely. If your application genuinely needs state across calls, the spec's guidance is to use explicit handles passed as tool arguments (a basket ID, a project ID), which has the side benefit of making that state visible to the model.
Do I have to implement `server/discover`?
To be a v2 server, yes: the spec requires it. To keep working today, no. The practical path is to implement it while keeping initialize as the compatibility shim.
Is SSE still supported?
The old HTTP+SSE transport was already deprecated in 2025-03-26, and v2 removes persistent server-to-client streams from the core. Server-initiated prompts now use multi round-trip requests with an echoed requestState instead, so any instance can process the retry. Building on SSE today means building on something the specification has just removed.
How long do roots, sampling, and logging keep working?
At least twelve months from the release, and removing them requires a separate proposal. The new feature lifecycle policy sets that minimum for anything the spec deprecates in future.
Related reading
- How to choose an MCP server for your AI agent
- What is an MCP server in AI? A clear, practical explainer
- Give your AI agent its own server with one MCP call
- MCP tools should do more than fetch data. They should ship software.
- Sandboxes vs build servers: what agents actually need