MCP 2026-07-28: Wetin Stateless Server Change?
MCP revision 2026-07-28 remove sessions and initialize handshake. See wetin change for reverse proxy, health checks, timeouts and auth.
Wetin stateless MCP server be
A stateless MCP server no dey keep state for each client between requests. Every request carry the protocol version, the client capabilities and the credentials wey server need to answer am, so any process for any machine fit answer any request. MCP (Model Context Protocol, the wire format wey agents dey use reach tools) make this a rule for revision 2026-07-28, wey remove the initialize handshake and the HTTP session wey dey underneath am. Everything for here concern the server side of that wire, so if agent side still new to you, one staged way to learn AI agents cover the loop wey decide to call tool before any of this HTTP detail matter.
Na this be the full operational point. Server wey no keep anything for each client fit stay behind ordinary load balancer without session affinity, restart during deploy without breaking clients, and run as four identical processes instead of one. Session-oriented server no fit do any of these things without extra machinery.
The Model Context Protocol na stateless protocol: all the information wey request need to process am dey inside the request itself. Server dey process each request independently; e no suppose infer state from previous requests, even requests wey use the same connection or stream.
Stateless no mean say your server no store anything. Your database, your queue and your cache still dey there. E mean say the protocol no carry state for the connection, so server no suppose treat connection, process or open socket as replacement for “this client, mid-conversation”. The difference dey easy to see for app wey already own its data: openGym read-only MCP server dey answer questions about training history wey dey inside the app own database, and nothing about that storage depend on the connection wey bring a particular request.
Wetin revision 2026-07-28 remove
2026-07-28 na the current revision of the specification as of August 2026. Compared with 2025-11-25, e remove five things wey dem add to support sessions.
- The
initializerequest and thenotifications/initializednotification. No handshake dey at all (SEP-2575). - The
Mcp-Session-Idheader, and session termination with HTTPDELETE(SEP-2567). - The standalone HTTP
GETstream wey servers dey use push notifications.subscriptions/listendon replace am; na normal POST wey response dey remain as long-lived stream. - SSE (server-sent events) stream resumability. The
Last-Event-IDheader and per-event IDs don go, so if stream break, the in-flight request go lost and client must issue am again as new request with new request ID. ping,logging/setLevelandnotifications/roots/list_changed. Log level don become per-request field,io.modelcontextprotocol/logLevelinside_meta.
Dem add one method, and every server must implement am. server/discover dey return the protocol versions, capabilities and identity wey server support, all for one call. Na this dey closest to handshake wey remain, and clients no need call am.
Why session transport hard to run for production
For 2025-11-25 and earlier, server fit create session ID when e initialize, then return am inside Mcp-Session-Id header for InitializeResult. Client then need send that header for every later request. Negotiated protocol version and client capabilities dey inside server memory, keyed by that ID. Each of these choices get operational cost.
- Restart throw away the session table. Specification require server to answer any request wey carry dead session ID with
404 Not Found, and require client to start again with newInitializeRequest. Every deploy become reconnect event for every connected client. - Second replica no know the sessions for the first replica. To scale out, you need sticky routing for the load balancer, or shared session store wey every replica go read for every request.
- Session table na memory wey dey grow as idle clients increase.
DELETEdey optional, and clients wey close without sending am leave entries behind. - List results fit differ for each connection, so caching in front of server no safe.
Removing sessions remove all four problems at once. Na this change you need understand before you touch any config.
Wetín every request dey carry now
Every POST go MCP endpoint stand on im own. Protocol version and client capabilities dey travel inside request body for _meta, and dem mirror selected fields inside HTTP headers so intermediary fit route based on dem without parsing JSON.
POST /mcp HTTP/1.1
Content-Type: application/json
Accept: application/json, text/event-stream
MCP-Protocol-Version: 2026-07-28
Mcp-Method: tools/call
Mcp-Name: get_weather
Authorization: Bearer <access token>
{
"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": {}
}
}
}io.modelcontextprotocol/protocolVersion and io.modelcontextprotocol/clientCapabilities dey required for every request. clientInfo no dey required, but clients suppose send am. If request miss any required field, request don malformed, so server must reject am with JSON-RPC error -32602 and HTTP 400 Bad Request.
Mcp-Method header dey required for every request. Mcp-Name dey required for tools/call, resources/read and prompts/get. Header value must match the body. If server process the body and values no match, server must reject am with 400 Bad Request and error code -32020, HeaderMismatch. This rule dey because load balancer wey route based on header and server wey execute based on body dey use two different sources of truth. If you route or rate-limit based on these headers, check MCP-Protocol-Version first. Earlier revisions no dey validate header against body, so header value no trustworthy for those versions.
Version disagreement don become normal per-request error instead of failed handshake. If server no implement the requested version, e answer 400 Bad Request with error -32022, UnsupportedProtocolVersion, and list the versions wey e support inside data.supported. Client go pick one from the list and retry.
State go where: tokens, cursors, subscriptions
State no disappear. E move go places wey you fit see and log.
Credentials dey enter every request. No session dey to attach identity to, so access token dey go with every HTTP call and system validate am every time. Details dey for authentication section below.
Cursors must carry their own position. Pagination for tools/list, resources/list, prompts/list and resources/templates/list dey use opaque cursor string, and clients no suppose parse or modify am. For single-process server, e common to keep offset for memory, keyed by session. Since no session dey, cursor must get enough information for any replica to continue the listing, so encode the position inside cursor and sign am, or keep am for storage wey every replica share. Invalid cursor suppose return -32602. Sign am because opaque cursor still be client-supplied input wey your code decode and trust.
Subscriptions belong to request, no be connection. Client wey want change notifications go send subscriptions/listen with filter wey name the types e want: toolsListChanged, promptsListChanged, resourcesListChanged and resourceSubscriptions. Server go reply with notifications/subscriptions/acknowledged and keep that response stream open. If stream drop, server no keep anything, and client go send subscriptions/listen again to get am back.
Cross-call application state turn to explicit handle. When server truly need remember something between calls, the specification answer na server-minted identifier wey e pass back as ordinary tool argument. E dey show for tool schema, e fit enter log, and connection no ever imply am. Server wey get real per-user data behind am, like self-hosted MCP email server, dey use this pattern instead of session: mailbox or draft identifier na tool argument, so any replica fit handle the next call. Plenty tools no need handle at all: search tool wey your own SearXNG instance dey back go take query and return results, with nothing for next call to resume and no reason to care which replica answer.
Deployment: reverse proxy, timeouts, health checks
MCP endpoint na one path wey dey accept POST. Most traffic na short request plus JSON response, and any proxy fit handle am. Exception na streaming response, where proxy defaults fit work against you. Na this part change when you move from laptop demo go MCP server wey dey run for VPS.
location /mcp {
proxy_pass http://127.0.0.1:8080;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_buffering off;
proxy_read_timeout 1h;
proxy_send_timeout 1h;
}proxy_buffering off matter because nginx dey buffer proxied responses by default. This one hold SSE events until buffer full or response end. Specification also ask servers to send X-Accel-Buffering: no for SSE responses, and nginx dey honour that header. So correct server fit tell your proxy the right thing by itself. Still set the directive, because na the part wey you control.
proxy_read_timeout default na 60 seconds. If subscriptions/listen stream stay quiet pass that time, nginx go close am, not your server. Your logs go show healthy process, but your client go show say stream drop. Raise am for MCP location only, not for the whole server. Servers also dey encouraged to send SSE comment line, meaning line wey start with colon, as keep-alive during quiet periods. This one stop intermediaries from timing out the stream at all.
Caddy need less configuration. By default, e dey buffer partially for wire efficiency, then flush immediately when response carry Content-Type: text/event-stream. So streaming dey work without extra directives.
mcp.example.com {
reverse_proxy 127.0.0.1:8080 {
health_uri /healthz
health_interval 10s
}
}Notice wetin that health check dey target. No use GET active check against MCP endpoint, because server wey implement only this revision go answer 405 Method Not Allowed to GET and DELETE, while Caddy default health method na GET. Proxy go then mark backend wey dey perfectly healthy as down. Serve plain path like /healthz for proxy, then check the protocol separately with POST.
curl -sS https://mcp.example.com/mcp \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-H 'MCP-Protocol-Version: 2026-07-28' \
-H 'Mcp-Method: server/discover' \
-d '{"jsonrpc":"2.0","id":"health-1","method":"server/discover","params":{"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientCapabilities":{}}}}'A 200 wey carry supportedVersions list mean say process dey up and dey speak the protocol. A 404 with JSON-RPC error -32601 mean say process dey up but e no serve server/discover, wey every 2026-07-28 server must implement. A 400 with -32022 mean say your checker ask for version wey this build no support. Na exactly this one you want catch after dependency upgrade. Open source nginx no get active health checks. So use passive max_fails and fail_timeout for upstream, then run protocol check from your monitoring instead.
Rolling restart now go cost you only requests wey dey in flight. Drain traffic, allow open POSTs finish, start the new process, and make clients send again anything wey fail. The only thing wey you still drop na any open subscriptions/listen stream, because that stream na live connection to one specific process. Statelessness remove session affinity. E no remove connection affinity for stream wey dey open now, and no routing rule fit fix that. Client fit tell the difference: stream wey end with empty subscriptions/listen result close gracefully. Stream wey end without one don drop, and client fit treat that as reason to reconnect.
Caching become possible for the first time. Results from list methods now carry ttlMs and cacheScope, while cacheScope: "public" tell shared intermediaries say dem fit cache the response. This one safe only because list results no longer vary per connection. Removing sessions directly cause this change.
Why authentication dey change when session no dey
When session dey, e easy to authenticate once at initialize, then treat the session ID as proof for everything after that. If you use session ID like that, e become bearer credential wey no get audience, expiry, or revocation path, and na your own server mint am. Removing sessions remove that shortcut, so the replacement dey stricter.
Protected MCP server dey act as OAuth 2.1 resource server. Every HTTP request from the client must carry Authorization: Bearer <access token>, and server go validate the token for every request. This validation include audience. Server must confirm say dem issue the token specifically for am, according to RFC 8707 (Resource Indicators for OAuth 2.0), and e must not accept or pass on tokens wey dem mean for another thing. Clients request the correct audience by sending resource parameter with the server canonical URI.
Discovery dey start from challenge. When request enter without usable token, server go answer 401 Unauthorized.
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer resource_metadata="https://mcp.example.com/.well-known/oauth-protected-resource",
scope="files:read"Client go read resource_metadata, fetch that document (RFC 9728, OAuth 2.0 Protected Resource Metadata, wey MCP servers must implement), find the authorization server, then run the flow. Valid token wey no get enough permissions go receive 403 Forbidden with error="insufficient_scope" and the scopes wey that operation need.
This one get two consequences for how you run am. Token validation now dey happen for every request instead of once per session. So, network round trip to introspection endpoint for every call go show for your latency. Prefer tokens wey you fit verify locally against signature, audience, and expiry. Or cache the validation result for short window, keyed by the token. And because no session dey hold identity, authorization must come from the token for every call. This one more honest than the session model, and e work together with the wider practice of keeping credentials away from the agent process. You fit read about am for how to keep secrets out of an AI agent. Scopes only limit wetin token fit do after request reach you. For the machine wey agent dey run on, harness plugins wey add tool permission rules and budget caps decide which calls dem make at all.
Wetin dey true for this revision, and wetin no be true
Everything wey dey above describe revision 2026-07-28. E no describe MCP forever, and e no describe the server wey you deploy last year.
Clients and servers for 2025-11-25 and earlier still dey use the handshake model. The specification call those revisions legacy, and call the per-request-metadata revisions modern. Server wey support only this revision, when e meet older client, suppose answer 405 Method Not Allowed to GET or DELETE for the MCP endpoint, ignore any Mcp-Session-Id header without creating or echoing one, and ignore Last-Event-ID because streams no dey resumable. Dual-era server fit serve both for one endpoint: request wey carry modern _meta go get stateless handling, while initialize request go select the older session semantics.
So check the revision string before you trust any of this. If your SDK still dey send initialize, sessions still dey real for your deployment and you still need manage the session-related problems wey dey above. The same thing apply for client side: agent process wey dey run for your own box, like the setup for running coding agent for VPS, na only stateless for this sense if the library wey e use dey speak modern revision. Read the version wey your runtime negotiate, then read the matching revision of the specification, and treat this page as description of one named revision, no be the protocol generally.
FAQ
Stateless MCP server mean say I no fit store anything?
No. Stateless describe the protocol, not your application. Databases, queues and caches still work exactly as before. The change be say state wey span several calls must use an explicit identifier wey client go pass for every request, like a server-minted handle inside a tool argument. Wetin you no fit do na infer context from the connection: the specification say server must not depend on earlier requests for the same connection to establish capabilities, protocol version or client identity, because every request dey provide dem for _meta.
I still need sticky sessions for my load balancer?
No, not for normal requests. Under revision 2026-07-28, every POST carry its own protocol version, capabilities and credentials. So any replica fit answer any request, and round-robin dey okay. The only long-lived thing wey remain na the subscriptions/listen response stream, and na one open connection to one process. E go end when that process end, then client go send subscriptions/listen again to establish am again. This na connection lifetime, no be session affinity, and no routing rule dey stop am.
Wetin happen to Mcp-Session-Id and the HTTP GET stream?
Dem both remove for revision 2026-07-28, under SEP-2567 and SEP-2575. Server wey implement only this revision suppose answer 405 Method Not Allowed to GET and DELETE for the MCP endpoint. E suppose ignore Mcp-Session-Id header instead of sending one back. Server-initiated change notifications now dey travel on the response stream of a subscriptions/listen request, instead of separate GET stream. Servers wey must continue to serve older clients go implement the earlier revision behavior alongside this one.
How I fit health check MCP server wey no get handshake?
Use two levels. Point the proxy active check to a plain HTTP path wey your application dey serve, because GET to the MCP endpoint correctly returns 405 and go mark healthy backend as down. Then check the protocol itself by POSTing server/discover. Every 2026-07-28 server must implement am. Confirm say the reply na HTTP 200 and say e list a protocol version wey your clients dey use. A 404 with JSON-RPC error -32601 mean the process dey run but e no dey serve that method. A 400 with -32022 mean the version wey you request no dey supported by that build.