EngineeringAugust 16, 2026·14 min read

Stateless MCP: Your Server Just Inherited the State

Stateless MCP dropped the handshake and Mcp-Session-Id in the 2026-07-28 spec. What it hands back: explicit state handles, expiry, per-request auth.

Title card for the post Stateless MCP: Your Server Just Inherited the State, with the three jobs a session used to do numbered as state container, lifecycle signal, identity key

On 28 July 2026 the Model Context Protocol removed its initialization handshake and the Mcp-Session-Id header. If you operate a remote MCP server, the deployment win landed the same day: requests are self-contained, any instance can serve any one of them, and the sticky routing you built around the old transport can go in the bin.

Stateless MCP is the 2026-07-28 specification's core change: every request now carries its own protocol version and client capabilities, so no server has to remember anything about the last one. The protocol got smaller. The state did not go anywhere. Cross-call state becomes an explicit handle your tools mint and the model passes back as an ordinary argument, and identity gets re-established on every single request. Sessions were quietly doing three jobs for you, and only one of them was transport plumbing.

Here is what changed, why the working group went further than "make sessions optional", and the three responsibilities that just landed on whoever owns the server.

What "stateless MCP" actually means in the 2026-07-28 spec

A stateless protocol is one where the receiver must not retain session state between requests: every request can be understood on its own. SEP-2575, the standards-track proposal that did the work, quotes that definition directly and makes the obvious comparison to HTTP, which is stateless while carrying most of the stateful applications on the internet.

Before, a client opened with an initialize call, got back a session, and then attached an Mcp-Session-Id header to everything that followed. That header pinned it to one server instance. Now the request carries what the handshake used to negotiate:

POST /mcp HTTP/1.1
MCP-Protocol-Version: 2026-07-28
Mcp-Method: tools/call
Mcp-Name: search
Content-Type: application/json

{"jsonrpc":"2.0","id":1,"method":"tools/call",
 "params":{"name":"search","arguments":{"q":"otters"},
           "_meta":{...clientInfo...}}}

Four details in that request matter more than they look.

MCP-Protocol-Version is mandatory on HTTP and has to match the value inside _meta, or the server must answer 400 Bad Request. Mcp-Method and Mcp-Name are new required headers, added so gateways can route and authorize on headers without parsing a JSON-RPC body. Client capabilities ride along on every request, and the spec is blunt that servers MUST NOT infer them from prior requests: an empty capabilities object means the client supports nothing optional, even if it declared otherwise thirty seconds ago. And version negotiation happens inline. Send your preferred version; if the server does not implement it you get UNSUPPORTED_PROTOCOL_VERSION (-32022) carrying the list of versions it does support, and you retry.

Worth knowing before you write the validator: client identity is softer than client capabilities. io.modelcontextprotocol/clientInfo was made optional after SEP-2575 was finalized, and clients now SHOULD send it unless they are configured not to. Reject a request for a missing protocol version. Do not reject one for a missing clientInfo.

There is a discovery RPC, server/discover, which returns supported versions, capabilities and usage instructions. Servers MUST implement it. Clients may ignore it entirely.

The practical effect, in the release-candidate post's words, is that a remote MCP server "can now run behind a plain round-robin load balancer". Cloudflare, which had been using Durable Objects to hold the stateful connection MCP required, now says MCP no longer needs one to speak the protocol and servers can run on request-scoped Workers. Netlify calls the result an ordinary HTTP workload. Nango, looking at it from the client side, counts the first tool call dropping from four HTTP round trips to two, and to one if the client has cached the tool list.

Those are platform and vendor posts, and they are all telling the true half of the story: the half where your infrastructure gets simpler.

Why sessions had to go, and not just become optional

The announcement coverage skipped the argument, which is a shame, because the argument is the most useful thing in the release. SEP-2567, the companion proposal that removed sessions themselves, makes a case that is worth reading even if you never touch MCP.

Nobody agreed what a session was. After more than a year in the spec, sessions had not converged on a consistent meaning across clients. ChatGPT created a fresh one for every individual tool call. Most desktop and IDE clients created one at application launch and held it for the process lifetime. Web clients typically created one per page load. Almost none resumed. If you wrote a server that tied a browser instance to "the session", you had no way to know whether that meant one user turn, one chat, or one week.

The reference SDK could not rehydrate one anyway. The TypeScript SDK's StreamableHTTPServerTransport stored session state in private instance fields with no API to restore it from external storage, so multi-node deployments could not honour resumption even when a client tried. Servers that appeared to be using session state successfully were usually stdio servers relying on process lifetime, which is a property of the transport rather than the protocol.

One session bought you exactly one of everything. Session state has a cardinality of one: the model gets one cart, one browser, one of whatever you scoped there. It cannot have two and it cannot have zero. SEP-2567's example is an orchestrator spawning subagents to research products. They should share a cart. They each need their own browser. No session boundary gives you both:

Session model Cart (want: shared) Browser (want: isolated)
Subagents share the parent's shared shared, and they clobber each other
Subagents get their own isolated, so the order splits isolated

And sessions taxed the clients that never used them. Because a tools/list result was allowed to vary by session, no client could safely cache one across a session boundary. For an orchestrator spawning short-lived subagents that meant O(subagents × servers) list calls, every subagent, every server, every time, even when the tool set was fixed at build time. Sessionless, the same workload is O(servers). The point that decided it: that cost came from sessions being possible, not from sessions being used, so making them opt-in would not have removed it.

Removing the session is what makes a list result a stable thing to cache. The same release then gave you the mechanics: tools/list, prompts/list, resources/list and resources/read responses now carry ttlMs and cacheScope (SEP-2549), so a client can hold a list for a stated period and invalidate on a list_changed notification instead of re-fetching on principle.

That is also why the working group rejected keeping an optional handshake. Two parallel interaction models would have meant two code paths in every server, every client and every SDK.

Transfer one: the session was your state container

With sessions gone, a server that needs state across calls mints an explicit handle. A creation tool returns an id, and subsequent tools take it as a parameter:

// → tools/call
{ "name": "create_basket", "arguments": {} }

// ← result
{ "content": [{ "type": "text", "text": "Created basket bsk_a1b2c3" }],
  "structuredContent": { "basket_id": "bsk_a1b2c3" } }

// → tools/call
{ "name": "add_item",
  "arguments": { "basket_id": "bsk_a1b2c3", "sku": "shoes" } }

The thing most summaries get wrong: this is not a protocol feature. There is no handles/* method and no handle type in the schema. From the wire's point of view basket_id is a string in a result and a string in an argument, indistinguishable from any other tool data. Explicit state handles are a tool-design pattern the spec documents and recommends, and the normative change is only the removal of sessions.

If that sounds familiar, it should. SEP-2567 points out that the large remote servers already work this way: Linear's create_issue returns an issue id, Notion returns a page id, GitHub returns a PR number, Stripe returns a customer id. Anyone who has designed a REST API in the last twenty years has written this pattern. The novelty is not the shape.

The novelty is where the handle lives. It sits in the model's context window, which means it also sits in the chat transcript, in subagent prompts, in whatever the user copied into Slack, and in the part of the conversation your client is about to compact away. SEP-2567 is direct about the client's job here: keep that string alive across compaction, because if the summariser drops it, the state it names is orphaned.

Three habits make handles behave. Keep them opaque, because a handle like cart_user42_2026-03-11 invites a model to guess the next one. Put the parameters on the creation call, so create_context(cluster="staging") beats create_context() followed by set_cluster() and the state can never exist half-configured. Ship a destroy_* and a list_* so a model that has lost track of what it made can recover.

Transfer two: the session was your lifecycle signal

A session ending was a free garbage-collection trigger. You do not get one now. A stateless server sitting behind a load balancer never sees a connection close.

So you own expiry. Pick a TTL, and then do the part that is easy to miss: write the durability policy into the tool's own description, not into your documentation. "Returns a basket_id; baskets expire after 24h idle" belongs where the model can read it at the moment it decides to create state. A policy that lives only in your docs site is invisible to the thing making the decision.

Then make the failure legible. When a tool gets a handle for state that has expired, the error should say basket bsk_a1b2c3 has expired, not invalid argument. A model that reads the first one calls create_basket again and carries on. A model that reads the second one retries the same broken call until something gives up.

Worth being honest about the baseline here: sessions never delivered reliable cleanup either. Against a per-tool-call client the state was destroyed before the next call; against a per-app-launch client it outlived every conversation in the window. Servers were already leaning on TTL-based expiry. The change is that the TTL is now the whole mechanism instead of a backstop, and the model can see it.

Transfer three: the session was your identity and correlation key

SEP-2575 puts the security consequence in one line: without a handshake, every request must be independently authenticated and authorized, and implementations MUST make sure authentication is not bypassed by the removal of the initialization phase.

The rule that follows is the one to tattoo somewhere. Possession of a handle is not authorization. For an authenticated server, validate the pair (handle, auth_context) on every call, because handles end up in chat logs and copy-paste buffers and other people's screens. The model for this is a Google Doc id or a GitHub PR number: the id names the resource, the auth context on the request decides access. For a server with no authentication, the handle is a bearer token, so generate it from a cryptographically secure source with at least 128 bits of entropy and give it a bounded lifetime.

If that feels like new work, look at what it replaced. SEP-2567 documents that the Python SDK's stateful session manager routed on Mcp-Session-Id alone, without checking that the authenticated identity on the request matched the one that created the session, so a leaked session id let any other authenticated principal hijack it. Session ids were already capability-bearing. The spec change did not create that requirement, it made it visible.

The rest of transfer three is bookkeeping, and it is the part that bites in production:

  • Telemetry and rate limits keyed on the session id need a new key. Use the authenticated principal (bearer-token subject, API key) or a request-level correlation id.
  • PKCE verifiers stored against a session move to a server-generated nonce carried in the OAuth state parameter, which was never an MCP request and never had the header anyway.
  • Session-to-user pinning can be deleted. It existed to defend against session routing being decoupled from auth, which stops being a problem once every request authenticates itself.

Alongside this, the same release hardened OAuth. Issuer validation is now required, client credentials are bound to the issuer that minted them, and Client ID Metadata Documents replace Dynamic Client Registration, which Cloudflare notes is slated for removal after summer 2027. WorkOS's walkthrough of the auth changes covers the RFCs involved if you are implementing the client half.

The removals that never make the changelog summary

The bullet-list recaps stop at the session header. These are the ones that will page someone:

  • Resumable SSE streams are gone. A dropped connection now implicitly cancels the request, and reconnecting with Last-Event-ID no longer resumes anything. On HTTP, closing the response stream is the cancellation signal. Anything long-running has to move to the Tasks extension, where a tools/call can return a task handle that the client drives with tasks/get, tasks/update and tasks/cancel. If you shipped against the experimental Tasks API in 2025-11-25, that lifecycle changed too.
  • ping is removed in both directions, on the reasoning that any normal RPC already proves the server is alive and transport keep-alives handle the rest.
  • resources/subscribe and resources/unsubscribe are gone, along with the HTTP GET channel for server-to-client messages. Everything is POST now. A client that wants notifications calls subscriptions/listen and opts in explicitly to each type; the server must not send a type that was not requested.
  • logging/setLevel is removed with no replacement RPC. Log level is a per-request _meta field, and if it is absent the server must send no log notifications for that request.
  • Your tool list may no longer change as a side effect of another call. The pattern where calling connect_database() makes query and list_tables appear in the next tools/list is not allowed. Expose them unconditionally and have them take a connection_id, and return an error pointing at connect_database when it is missing.
  • Servers can only initiate a request while processing one. Elicitation, sampling and roots round-trips are replaced by Multi Round-Trip Requests: the server returns input_required with a requestState token, and the client re-issues the original call with the answers attached. Any instance can pick up that retry, which is the whole point.

One thing to keep straight, because it is the easiest mistake to make when skimming the release notes. Roots, Sampling and Logging are deprecated, and the new policy guarantees them for at least twelve months. Sessions were removed, with no deprecation window at all. The migration path for a server that still needs session semantics is to stay on the older protocol version until it has moved to handles.

What to change, by server category

SEP-2567 ran an automated survey of a 1,000-repo random sample of open-source MCP servers, classified by per-repo LLM analysis, and sorted them by how much work the change creates:

What your code does with the session id Share What you do
Nothing; no application-level reference to it 90.0% Nothing
Map<sessionId, Transport> routing (TS SDK boilerplate) 3.5% Disappears with a sessionless SDK transport
Transport setup only (sessionIdGenerator, never read) 2.8% Delete one constructor option
Keys application state 2.5% Move to explicit handles, or to the auth principal
Sticky routing in a proxy or gateway 0.7% Needs a designed replacement
Binds auth artifacts (JWT claims, PKCE verifier) 0.5% Server-generated nonce, or the token subject

Read that top row with the methodology in mind: it is a sample of public repositories classified by a model, not a census of production deployments, and the servers doing the most interesting work are disproportionately the ones in the last three rows. Still, the shape is credible, and it matches the fact that every official SDK except PHP already had a stateless mode. sessionIdGenerator: undefined in TypeScript and stateless_http=True in Python were opt-in flags. The new spec version makes that the only mode.

Gateways are the genuinely hard case, and SEP-2567 says so: routing by session id needs a designed replacement rather than a mechanical edit. The consolation is that a gateway only needed a sticky routing key because its upstream was stateful. Once the upstream keys its state on a handle that any replica can look up in shared storage, the sticky routing has nothing left to do. The residual case is bridging HTTP to a stdio subprocess, which needs its own correlation key at the transport layer.

If you are supporting both protocol versions for a while, the fallback differs by transport. On HTTP, send a new-style request and fall back when you get a 400. On stdio there is no per-request error to key off, so probe with server/discover first.

The order we would work in: grep for Mcp-Session-Id and sessionIdGenerator to find out which row of that table you are in; add the create_* tool and its destroy_* before touching anything else, so the state has somewhere to live; move authorization to (handle, auth_context) on every call before you advertise the new protocol version; then re-key telemetry last, because that is the one that fails quietly.

What the spec still refuses to decide for you

StackOne, who sell tooling in this space and so have a stake in the answer, make a fair point about what the specification deliberately leaves out. It standardises how a tool is described, called and transported. It says nothing about whether a given agent should be allowed to run a given action against a system of record, who approved a write, or what was logged and whether you can still query it in six months. Their view is that this work lives at the execution and governance layer, and they are sceptical that a human approval prompt survives contact with volume.

We think that boundary is correct for a protocol, and we would rather it stay there than watch a standards body try to model your approval workflow. It is also worth noticing that statelessness makes that layer easier to build. Every request now carries its own identity and announces its method and target in headers, so a gateway can authorize tools/call on payments.refund without terminating a session or parsing a body. That is the same move as routing LLM traffic through a provider-agnostic gateway: the protocol keeps getting simpler and the operational surface keeps sliding toward the team running the thing.

Frequently asked questions

Is MCP fully stateless now?

No, and the spec is careful to say "stateless by default". SSE streams still carry several messages inside a single HTTP request, so there is state inside that window. The difference is that the complexity is constrained to one request and optional to use.

Do I have to rewrite my MCP server?

Probably not. In SEP-2567's survey, 90% of sampled servers made no application-level reference to the session id at all. Grep for Mcp-Session-Id and sessionIdGenerator: if neither appears outside transport setup, your migration is an SDK upgrade.

What replaces Mcp-Session-Id for keeping state between tool calls?

An explicit state handle. A creation tool returns an opaque id, later tools accept it as an argument, your server owns the underlying state, and the model carries the name. The protocol has no idea any of this is happening.

Does removing sessions make my server less secure?

It changes the exposure surface rather than introducing a new class of vulnerability. Handles reach places session ids did not, so validate (handle, auth_context) on every call. That was already the correct posture for session ids, and at least one reference SDK got it wrong.

How long until the old protocol version stops working?

Two different clocks. Deprecated features (Roots, Sampling, Logging, the legacy HTTP+SSE transport) are guaranteed for at least twelve months under the new deprecation policy. Session removal came with no window, so a server that depends on session semantics stays on the older protocol version until it migrates.

Pin the version before you chase the feature

Stateless MCP is a good change, and the deployment story genuinely improves. The part worth planning for is that a protocol version bump is a behaviour change with an agent on the other end of it, which puts it in the same category as swapping the model underneath a feature. We argued previously for putting an eval gate in front of a model swap; the same reasoning applies to a client that quietly stops declaring a capability it used to have.

So before you migrate: pin the protocol versions your server accepts, log the version and client identity that every request actually sends, and keep the old path alive until that log tells you nobody is using it. The header is right there on every request now. You may as well read it.

If you are building agent infrastructure and want a team that has shipped this kind of thing before, that is what we do at Vantaso, or say hello at contact@vantaso.org.

Sources

  1. SEP-2575 'Make MCP Stateless' (Final, Standards Track) removes the initialization handshake. Its motivation is that a stateless load balancer cannot be used with a stateful protocol because it would route a client's requests to different backend servers, none of which hold the session state, forcing operators into sticky sessions; it also cites session state being lost when an instance fails, and per-client session bookkeeping being a common source of bugs and memory leaks. Design principles in priority order: prioritize statelessness so a request is self-contained, prefer state references, and treat statefulness as a last resort. On HTTP the MCP-Protocol-Version header is mandatory and must match the request payload's _meta value or the server MUST return 400 Bad Request. A client MUST specify its capabilities on every request and servers MUST NOT infer capabilities from prior requests, an empty capabilities object meaning the client supports no optional capabilities. Per-request _meta carries io.modelcontextprotocol/protocolVersion (required), clientInfo, clientCapabilities and an optional logLevel, whose semantics read: 'the desired log level for this request. Optional. If absent, the server MUST NOT send any log notifications for this request.' New error codes are UNSUPPORTED_PROTOCOL_VERSION (-32022), which returns the server's supported version list, and MISSING_REQUIRED_CLIENT_CAPABILITY (-32021). Servers MUST implement server/discover; clients MAY call it but are not required to. Removed RPCs: initialize and notifications/initialized; logging/setLevel with no replacement RPC; roots/list as a top-level server-to-client RPC; notifications/roots/list_changed; resources/subscribe and resources/unsubscribe, because resource subscriptions are inherently stateful; and ping in both directions, because any normal RPC already proves server liveness. The HTTP GET endpoint for server-to-client messages is removed, all communication uses POST, and subscriptions/listen replaces it with explicit per-type opt-in. Resumable SSE streams via Last-Event-ID are removed because connection drops now implicitly cancel a request, and workloads needing durability or resumability MUST use the tasks primitive; on HTTP, closing the SSE response stream MUST be treated as cancellation. Security implications: without a session handshake, every request must be independently authenticated and authorized, and implementations MUST ensure authentication is not bypassed by the removal of the initialization phase. The FAQ answers that MCP is not entirely stateless, hence 'by default', since SSE streams carry multiple messages within a single constrained and optional HTTP request, and notes that a stateless protocol does not prevent building stateful applications on top, HTTP being the example. An optional handshake was considered and rejected because supporting two parallel interaction models would have dramatically increased the complexity of the protocol and every implementation. For mixed deployments, a server MAY keep implementing initialize for legacy clients; on HTTP a dual-version client can attempt a new-style request and fall back on 400, while on STDIO it SHOULD probe with server/discover first. Post-finalization, PR #3002 made io.modelcontextprotocol/clientInfo optional, with clients SHOULD including it on every request unless specifically configured not to.
  2. SEP-2567 'Sessionless MCP via Explicit State Handles' (Final, Standards Track) removes the protocol-level session concept and the Mcp-Session-Id header, replacing implicit session-scoped state with explicit, server-minted state handles that the model carries and threads through subsequent calls. Motivation: after more than a year in the spec, sessions had not converged on a consistent meaning across clients, with some scoping them per tool call, some per application launch, some per page load, and almost none resuming them, which made the session unreliable as a container for application state. Concretely, ChatGPT creates a fresh session for every individual tool call and Claude.ai did the same until recently, most desktop and IDE clients create one at application launch, and web clients typically create one per page load. The reference TypeScript SDK's StreamableHTTPServerTransport stores session state in private instance fields with no API to rehydrate it from external storage, so multi-node deployments cannot honor resumption; servers that appear to use session state successfully are usually stdio servers relying on process lifetime, a property of the transport rather than the protocol. Session state has a cardinality of exactly one per session, so an orchestrator whose subagents must share a cart but need separate browsers cannot be satisfied by any session boundary. Because a tools/list result could vary by session, the possibility of session scoping forced O(subagents x servers) list calls, which becomes O(servers) once sessions are removed; that cost came from sessions being possible rather than used, which is why making them optional would not have removed it. Servers may no longer mutate list results as a side effect of other requests, so the pattern where connect_database() makes query and list_tables appear in the next tools/list is not permitted; the replacement is to expose those tools unconditionally and have them take a connection_id. Handles are explicitly not a protocol feature: there is no handles/* method, no handle type in the schema, and from the protocol's perspective a handle is a string in a tool result and a string in a tool argument. The canonical shape is create_basket() returning basket_id in structuredContent, then add_item(basket_id, sku) and checkout(basket_id), a pattern already used by Linear, Notion, GitHub and Stripe remote servers. Non-normative server guidance: keep handles opaque, since a handle like cart_user42_2026-03-11 invites clients to parse it or models to guess it; possession is not authorization, so authenticated servers should validate (handle, auth_context) on every call because handles end up in chat logs, copy-paste buffers and subagent prompts, while unauthenticated servers must treat the handle as a bearer token generated with at least 128 bits of cryptographically secure entropy and a bounded lifetime; document durability in the create tool's description, since a policy only in server documentation is not visible to the model; return useful expiry errors such as 'basket bsk_a1b2c3 has expired' rather than 'invalid argument'; put parameters on the creation call so state cannot exist half-configured; and optionally provide destroy_* and list_* tools. The main client responsibility is ensuring the handle string survives context compaction, since a summarized conversation that discards it orphans the state. On garbage collection, sessions did not deliver it reliably in practice and stateless HTTP servers behind load balancers never see a connection close, so servers already rely on TTL-based expiry. On security, handle exposure is a change in exposure surface rather than a new class of vulnerability, and the Python SDK's stateful session manager routed by Mcp-Session-Id alone without verifying that the authenticated identity matched the one that created the session, so a leaked session id allowed hijack by any other authenticated principal. An automated survey of a 1,000-repo random sample of open-source MCP servers, classified by per-repo LLM analysis, found 90.0% with no application-level reference to the session id, 3.5% using Map<sessionId, Transport> routing that a sessionless SDK transport removes, 2.8% with transport setup only, 2.5% keying application state on the session, 0.7% doing proxy or gateway sticky routing, and 0.5% binding auth artifacts such as JWT claims or a PKCE verifier; the hardest-hit category, gateways spawning one upstream per session, needs a designed replacement rather than a mechanical edit. Migration guidance re-keys session-scoped telemetry onto the authenticated principal or a request-level correlation id, moves PKCE verifiers to a server-generated nonce in the OAuth state parameter, and notes session-to-user pinning is unnecessary once every request is independently authenticated. All official SDKs except PHP already provided a stateless mode via sessionIdGenerator: undefined in TypeScript and stateless_http=True in Python, and the new protocol version makes that the only mode. Rollout is a clean break with no deprecation window: servers relying on session-scoped state stay on the current protocol version until they migrate.
  3. The 2026-07-28 MCP specification retires the initialize/initialized exchange and the Mcp-Session-Id header, so each request travels on its own carrying its protocol version, client identity and client capabilities. It adds required Mcp-Method and Mcp-Name headers so gateways can route and authorize on headers directly, adds ttlMs and cacheScope to tools/list, prompts/list, resources/list and resources/read responses, and replaces server-initiated elicitation/create, sampling/createMessage and roots/list with Multi Round-Trip Requests in which the server returns resultType input_required along with the requests it needs answered. Tasks moves from experimental core into the io.modelcontextprotocol/tasks extension with poll-based tasks/get and tasks/update. Roots, Sampling and Logging are formally deprecated, Dynamic Client Registration is deprecated in favor of Client ID Metadata Documents, and the legacy HTTP+SSE transport is officially deprecated. Authorization hardening requires RFC 9207 issuer validation, which clients must validate before redeeming a code, and binds client credentials to the issuer that minted them. The new deprecation policy provides a twelve-month minimum window, and deprecated features keep working for at least twelve months although new implementations should not adopt them. The post also states that dropping the protocol-level session does not force your application to be stateless, and that a server needing to carry state across calls should mint an explicit handle.
  4. The release-candidate post shows the before and after: previously an initialize POST established a session and subsequent requests carried an Mcp-Session-Id header pinning the client to a specific server instance; now a request is self-contained, sending MCP-Protocol-Version, Mcp-Method and Mcp-Name headers with clientInfo in the payload's _meta. The stated effect is that any MCP request can land on any server instance and the sticky routing and shared session stores horizontal deployments needed are no longer required at the protocol layer, so a remote MCP server can now run behind a plain round-robin load balancer, route traffic on an Mcp-Method header, and let clients cache tools/list responses. Server-initiated requests may only be issued while the server is actively processing a client request: the server returns an InputRequiredResult with a requestState token, and the client re-issues the original call with inputResponses and the echoed requestState, which any server instance can pick up because everything it needs is in the payload. Tasks graduated to an extension where a server can answer tools/call with a task handle that the client drives with tasks/get, tasks/update and tasks/cancel, and anyone who shipped against the 2025-11-25 experimental Tasks API must migrate to the new lifecycle. Roots, Sampling and Logging are annotation-only deprecations whose methods, types and capability flags continue to work in this release and in every specification version published within a year of it, with replacements being tool parameters or resource URIs or config, direct LLM API integration, and stderr or OpenTelemetry respectively. On carrying state, it advises minting an explicit handle such as a basket_id or browser_id from a tool and having the model pass it back as an ordinary argument on later calls.
  5. Cloudflare states that the new protocol removes the required handshake, the Mcp-Session-Id header and protocol sessions from the core request path, and that building a well-behaved MCP server previously meant managing request routing to sticky sessions, holding open streams, message replay, and more overhead than a traditional web server. Cloudflare had used Durable Objects for MCP because they combine compute, persistent transactional storage and real-time coordination and can hibernate when not in use while keeping the stateful connection MCP needed; now MCP itself no longer requires a Durable Object to speak the protocol and servers can scale on request-scoped infrastructure such as Cloudflare Workers. On authorization the spec prefers pre-registered clients where a relationship already exists, then Client ID Metadata Documents for dynamic registrations, with Dynamic Client Registration as a fallback that is deprecated for new implementations and slated for removal after summer 2027.
  6. Netlify describes the 2026-07-28 specification as making the protocol stateless at its core, turning an MCP server into an ordinary HTTP workload with no sticky sessions, no session store and no shared state across instances, where previously servers needed session plumbing and infrastructure designed around long-lived connections. It also notes the spec formalizes the Extensions framework, with MCP Apps and Tasks becoming the first official extensions.
  7. Nango reports that with the handshake gone every MCP request is self-contained and any instance behind a load balancer can serve it, and counts the first tool call dropping from four HTTP round trips to two, or to one if the client caches the tool list. Its implementation notes recommend returning tools in deterministic order for stable prompt caching and using the Tasks extension to return a durable taskId rather than blocking, since connection drops now cancel in-flight requests.
  8. WorkOS's analysis of the 2026-07-28 authorization changes describes MCP servers acting as an OAuth 2.0 Protected Resource exposing Protected Resource Metadata (RFC 9728), clients implementing Resource Indicators (RFC 8707) to designate which server a token targets and prevent replay across servers, required issuer verification (RFC 9207) addressing mix-up attacks with credentials bound to a specific issuer, Client ID Metadata Documents becoming the preferred registration method with Dynamic Client Registration deprecated, and clients declaring an OpenID Connect application_type so authorization servers stop defaulting desktop and CLI clients to web and rejecting localhost redirect URIs.
  9. StackOne argues that the specification standardizes how a tool is described, how it is called and how that call is transported, and says nothing about whether a given agent should run a given action against a system of record, who approved a write to a system of record and how, or what was logged and whether it is still queryable six months later. Their framing is that this authorization and audit work is left to the deployer and lives at the execution and governance layer rather than in the protocol, and that a human approval prompt does not hold up under volume because approval clicks degrade with attention.

Get Started

Ready to discuss your project with us?

The future of your industry starts here.

Contact Us