The Model Context Protocol has stopped behaving like a promising experiment and started behaving like infrastructure. The 2026-07-28 specification removes protocol sessions, makes every request self-describing, tightens the OAuth story and moves enterprise features into formal extensions. The MCP team reports that its Tier 1 SDKs see close to half a billion downloads a month, and that the TypeScript and Python SDKs have each passed one billion total downloads. At that scale, a spec revision is an operations event, not a changelog footnote.
If you run an MCP server, a client or an agent that calls both, some of this release is breaking. This post covers what changed, why it matters, and what to do about it this quarter.
Why this release is different
Earlier revisions added features. This one changes the shape of the protocol so it fits the infrastructure teams already run: load balancers, API gateways, WAFs, identity providers and caches.
Three themes run through the release:
- Stateless by default. Any request can land on any server instance.
- Standard OAuth, applied strictly. MCP servers are OAuth 2.1 resource servers, and clients have to prove they asked for the right token from the right issuer.
- Extensions for the enterprise. Tasks, Enterprise-Managed Authorization and MCP Apps now live in a formal extensions framework instead of being bolted onto the core.
The spec also adopts a feature lifecycle and deprecation policy with a minimum twelve-month deprecation window. That matters as much as any single feature, because it tells you how long you can lean on something before you have to move.
The stateless core: no handshake, no sessions
The biggest change is the removal of the initialize and notifications/initialized handshake, along with the Mcp-Session-Id header. According to the official changelog, every request now carries its protocol version and client capabilities in _meta, under io.modelcontextprotocol/protocolVersion and io.modelcontextprotocol/clientCapabilities. Clients should also send io.modelcontextprotocol/clientInfo, and servers should return io.modelcontextprotocol/serverInfo in each result.
Servers must implement a new server/discover method that advertises supported versions, capabilities and identity. Clients may call it up front, but they don't have to.
What that looks like on the wire
The Streamable HTTP transport now also requires request metadata headers. Mcp-Method goes on every request, and Mcp-Name goes on tools/call, resources/read and prompts/get. A tool call looks like this:
POST /mcp HTTP/1.1
Content-Type: application/json
Accept: application/json, text/event-stream
Authorization: Bearer <access-token>
MCP-Protocol-Version: 2026-07-28
Mcp-Method: tools/call
Mcp-Name: search_tickets
{
"jsonrpc": "2.0",
"id": 7,
"method": "tools/call",
"params": {
"name": "search_tickets",
"arguments": { "query": "refund", "limit": 10 },
"_meta": {
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
"io.modelcontextprotocol/clientInfo": { "name": "support-agent", "version": "2.3.0" },
"io.modelcontextprotocol/clientCapabilities": {}
}
}
}The headers exist so gateways can route, rate-limit and meter traffic without parsing JSON. The catch is that servers must reject any request where the headers and body disagree, returning 400 Bad Request with the HeaderMismatch error code -32020. The same page notes that tools can mirror selected arguments into Mcp-Param-{Name} headers through an x-mcp-header annotation in the input schema, which is useful for routing by region or tenant.
Where did the state go?
Real applications still need state. The spec's answer is to make it explicit. Servers that need cross-call state mint their own handles and pass them as ordinary tool arguments. A start_export tool returns an export_id, and get_export takes it. That is more honest than hiding state in a transport session, and it survives a pod restart if you store it properly.
Other pieces of the old stateful model changed too:
| Old mechanism | 2026-07-28 replacement |
|---|---|
initialize handshake |
Per-request _meta plus optional server/discover |
Mcp-Session-Id header |
Removed. Use server-minted handles in tool arguments |
| Server-initiated requests (sampling, elicitation, roots) | Multi Round-Trip Requests with resultType: "input_required" |
HTTP GET stream and resources/subscribe |
subscriptions/listen on a long-lived POST response stream |
logging/setLevel |
Per-request io.modelcontextprotocol/logLevel in _meta |
Last-Event-ID resumability |
Removed. Clients re-issue the request with a new ID |
Multi Round-Trip Requests (MRTR) deserve a closer look. When a tool needs more input partway through, the server returns an InputRequiredResult whose inputRequests field lists what it needs. The client gathers the answers and retries the original call with inputResponses. Every result now carries a required resultType of "complete" or "input_required", and clients must treat results from older servers that omit the field as complete.
List results got cheaper too. tools/list, prompts/list, resources/list, resources/read and resources/templates/list now require ttlMs and cacheScope fields, so clients and shared intermediaries know how long they can cache a response. Servers should also return tools in a deterministic order, which the changelog notes improves LLM prompt cache hit rates.
MCP servers are OAuth 2.1 resource servers
MCP servers were first classified as OAuth resource servers in the 2025-06-18 revision. The 2026-07-28 authorization spec keeps that model and makes the edges sharper.
The division of labor is simple. The MCP server accepts and validates access tokens. The authorization server authenticates the user and issues tokens. The client obtains tokens on the user's behalf. They can run in the same deployment or as separate services, but they are different roles, and your code should treat them that way.
On the server side, the non-negotiables are:
- Validate every access token per OAuth 2.1, including that it was issued for this server as the audience.
- Return
401for invalid or expired tokens. - Never accept or pass through tokens that were minted for someone else.
- Return
403witherror="insufficient_scope"and the full set of required scopes when a token is valid but not sufficient, so the client can step up in one round trip.
Here is a minimal token check in TypeScript using the jose library. The important line is the audience check.
import { createRemoteJWKSet, jwtVerify } from "jose";
const ISSUER = "https://auth.example.com";
const RESOURCE = "https://mcp.example.com/mcp"; // canonical server URI
const jwks = createRemoteJWKSet(new URL(`${ISSUER}/.well-known/jwks.json`));
export async function authenticate(req: Request) {
const token = req.headers.get("authorization")?.replace(/^Bearer /, "");
if (!token) return unauthorized();
try {
const { payload } = await jwtVerify(token, jwks, {
issuer: ISSUER,
audience: RESOURCE, // reject tokens minted for any other server
});
return payload;
} catch {
return unauthorized();
}
}
function unauthorized() {
return new Response(null, {
status: 401,
headers: {
"WWW-Authenticate":
'Bearer resource_metadata="https://mcp.example.com/.well-known/oauth-protected-resource/mcp", scope="tickets:read"',
},
});
}If your authorization server issues opaque tokens, the same rule applies through token introspection: check the audience, not just validity.
Protected Resource Metadata and Resource Indicators
Two RFCs do most of the work of connecting clients to the right authorization server and keeping tokens where they belong.
RFC 9728: tell clients where to get tokens
MCP servers must implement OAuth 2.0 Protected Resource Metadata, and the document must include an authorization_servers field with at least one entry. The discovery rules say servers must expose it either through resource_metadata in the WWW-Authenticate header of a 401, or at a well-known URI. For an endpoint at https://example.com/public/mcp, that is https://example.com/.well-known/oauth-protected-resource/public/mcp, with the root path as a fallback. Clients must support both and try them in that order.
A small metadata document is enough:
{
"resource": "https://mcp.example.com/mcp",
"authorization_servers": ["https://auth.example.com"],
"scopes_supported": ["tickets:read"],
"bearer_methods_supported": ["header"]
}Keep scopes_supported minimal. The spec describes it as the set needed for basic functionality, with extra scopes requested later through step-up authorization. It also advises servers not to list offline_access there.
Clients then discover the authorization server's own metadata by trying OAuth 2.0 Authorization Server Metadata and OpenID Connect Discovery endpoints in a defined order, and must reject any document whose issuer does not exactly match the issuer they used to build the URL.
RFC 8707: bind every token to one server
Clients must send the resource parameter in both the authorization request and the token request, set to the canonical URI of the MCP server. The spec requires clients to send it even if the authorization server does not advertise support. Canonical means an absolute URI with a scheme and no fragment, such as https://mcp.example.com/mcp, and the spec recommends dropping the trailing slash unless it is meaningful.
The point is containment. A token minted for one server cannot be replayed against another, because the audience will not match. That is why the server-side audience check above is not optional.
Hardening the client side: issuers, CIMD and application types
The rest of the authorization changes land mostly on clients.
Issuer validation (RFC 9207). Authorization servers should include iss in authorization responses, and clients must validate a present iss against the issuer they recorded before redirecting the user, using exact string comparison with no normalization. If the server advertises authorization_response_iss_parameter_supported: true and iss is missing, the client must reject the response. The spec says a future revision is expected to make iss a MUST for authorization servers, so emit and check it now.
Credentials bound to their issuer. Clients must key stored client credentials by the authorization server's issuer, must not reuse them with a different authorization server, and must re-register when protected resource metadata points somewhere new. This closes off mix-up attacks in setups with several authorization servers.
Client ID Metadata Documents replace DCR as the default. Dynamic Client Registration is now deprecated, kept only for authorization servers that don't support the new approach. With a Client ID Metadata Document (CIMD), the client_id is an HTTPS URL that points to a JSON document containing at least client_id, client_name and redirect_uris. Authorization servers advertise support with client_id_metadata_document_supported. The recommended order is pre-registered credentials first, then CIMD, then DCR, then asking the user. CIMD client IDs are portable across authorization servers, so moving servers does not force re-registration.
Declare application_type if you still use DCR. Omitting it defaults to "web" under OpenID Connect, which breaks localhost redirects for desktop apps and CLIs. Native clients should send "native".
Enterprise-Managed Authorization
Per-user consent screens work for consumers. They don't work for a company rolling agents out to thousands of employees. The Enterprise-Managed Authorization extension (io.modelcontextprotocol/enterprise-managed-authorization) makes the organization's identity provider the place where access decisions happen.
The flow works like this:
- The user signs in to the MCP client through corporate SSO, and the client keeps the resulting identity assertion.
- The client exchanges that assertion with the IdP for an Identity Assertion JWT Authorization Grant (ID-JAG). The IdP checks policy first.
- The client presents the ID-JAG to the MCP server's authorization server and receives an access token. The user never sees a per-server consent screen.
Revoking access at the IdP applies across every client. The MCP team announced the extension as stable on June 18, 2026, with Okta as the first supported identity provider, support in Claude and Visual Studio Code, and servers from Asana, Atlassian, Canva, Figma, Granola, Linear and Supabase.
What it asks of you:
- Clients declare the extension in
clientCapabilities.extensions, support SSO, store the identity assertion, and let admins configure the IdP at the organization level. - Servers declare the extension in their authorization metadata.
- Authorization servers validate ID-JAG signatures, audience, issuer and expiry, map claims to permissions, and link accounts by the subject claim, with email as a fallback for accounts created before EMA was set up.
If you sell to enterprises, expect this to show up in security reviews.
Registry and ecosystem growth
Discovery is growing up alongside the protocol. The official MCP Registry hosts metadata, not code. Each server is described in a standard server.json that says where to find it, such as an npm package or a remote URL. Names use reverse DNS, like com.example/server, and publishers prove ownership through GitHub, DNS or HTTP challenges. The registry is built mainly for downstream aggregators and marketplaces, which add curation and ratings. It does not accept private servers, and the docs recommend running your own registry that implements the same OpenAPI spec for internal use.
Keep one caveat in mind: the registry is still labeled preview, and the docs warn that breaking changes or data resets may happen before general availability. Publish to it, but don't make it a hard runtime dependency yet.
The extensions model is the other growth path. Tasks moved into io.modelcontextprotocol/tasks with polling through tasks/get and a new tasks/update, and MCP Apps sit alongside EMA as formal extensions. Extensions are opt-in and versioned on their own, so the core can stay small.
Migration checklist and pitfalls
Servers
- Accept per-request
_metaand implementserver/discover. - Stop minting or relying on
Mcp-Session-Id. Answer GET and DELETE on the MCP endpoint with405. - Move any session state into explicit handles backed by a real store.
- Validate
MCP-Protocol-Version,Mcp-MethodandMcp-Nameagainst the body and return-32020on mismatch. - Return
resultTypeon every result, andttlMspluscacheScopeon list and read results. - Replace server-initiated sampling and elicitation with MRTR.
- Publish Protected Resource Metadata and check token audience on every request.
Clients
- Send
_metaand the required headers on every request, and supportx-mcp-headermirroring. - Send
resourcein authorization and token requests. - Record the expected issuer before redirecting, and validate
isson return. - Key stored credentials by issuer. Prefer CIMD, and set
application_typeif you fall back to DCR. - Handle
403 insufficient_scopeby requesting the union of old and new scopes, with a retry limit. - For older servers, try a modern request first and fall back to
initializeonly when the400body is not a recognized modern error, as the backward compatibility section describes.
Pitfalls we expect to see
- Header and body drift. A proxy that rewrites the body, or a client that caches headers, will trigger
HeaderMismatch. Build headers from the final body. - Trailing slashes.
https://mcp.example.com/andhttps://mcp.example.comare different strings. Pick one canonical form and use it in metadata, theresourceparameter and audience checks. - Normalizing
iss. The spec forbids case folding or slash trimming before comparison. Plain string equality only. - Silent state. Removing the session header does not remove hidden in-memory caches keyed by connection. Audit for them before you scale horizontally.
- Leaning on deprecated features. Roots, Sampling and Logging are deprecated. They still work during the window, but new code should pass paths as tool parameters, call LLM provider APIs directly, and log to
stderror OpenTelemetry. - Assuming the SDK did it all. The TypeScript, Python, Go and C# SDKs support 2026-07-28, with Rust in beta, per the release post. An SDK upgrade covers the wire format. It does not move your session state or fix your audience checks.
What to do this quarter
- Inventory every MCP server and client you run, and note the protocol version each speaks.
- Upgrade SDKs to 2026-07-28 behind a flag and run both versions side by side.
- Remove session affinity from your load balancer once servers are stateless.
- Publish Protected Resource Metadata and add audience checks to token validation.
- Add
resourceandisshandling to every OAuth client, and plan the move from DCR to CIMD. - Replace Sampling, Roots and Logging usage in new code.
- If you sell to enterprises, scope Enterprise-Managed Authorization support.
- Publish public servers to the MCP Registry, treating it as preview.
Where this leaves agent teams
The 2026-07-28 release makes MCP easier to run and harder to get wrong. Stateless requests scale on ordinary infrastructure, strict OAuth keeps tokens inside their intended boundary, and EMA gives security teams one place to set policy. The cost is a real migration, mostly in auth code and anything that quietly relied on sessions.
Some teams will build all of this themselves. Others would rather not run OAuth plumbing for every third-party app their agents touch. Hoook is one option for that second group: it provides hosted MCP servers with one URL per user, and handles end-user auth through a Connect Link with encrypted token storage and automatic refresh. Whichever route you take, the checklist above is the same. Start with token audience and session state, because those are the two places this spec is least forgiving.