SSD Nodes Learn 🎉 VPS from $5.50/mo
How to do am Matt ConnorBy Matt Connor · Updated 2026-08-13

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

Stateless MCP server no dey keep any state for each client between requests. Every request dey carry protocol version, client capabilities and credentials wey server need to answer am, so any process for any machine fit answer any request. MCP (Model Context Protocol, na the wire format wey agents dey use to reach tools) make this a rule for revision 2026-07-28, wey remove initialize handshake and HTTP session wey dey under am.

Na this be the main operational point. Server wey no dey 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.

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 any state from previous requests, even requests wey dey use the same connection or stream.

Stateless no mean say your server no dey store anything. Your database, queue and 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”.

Revision 2026-07-28 remove wetin

2026-07-28 na the current revision of the specification as of August 2026. Compared with 2025-11-25, e remove five things wey dey support sessions before.

  • The initialize request and the notifications/initialized notification. No handshake dey at all (SEP-2575).
  • The Mcp-Session-Id header, and session termination with HTTP DELETE (SEP-2567).
  • The standalone HTTP GET stream wey servers dey use push notifications. subscriptions/listen replace am; na ordinary POST wey response be long-lived stream.
  • SSE (server-sent events) stream resumability. The Last-Event-ID header 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/setLevel and notifications/roots/list_changed. Log level now dey as per-request field, io.modelcontextprotocol/logLevel for _meta.

Dem add one method, and every server must implement am. server/discover dey return the server supported protocol versions, capabilities, and identity for one call. Na the closest thing to handshake wey remain, and clients no need call am.

Why e hard to run session transport for production

For 2025-11-25 and earlier versions, server fit create session ID when e initialize, then return am for 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 dey throw away the session table. Specification require server answer any request wey carry dead session ID with 404 Not Found, and require client start again with new InitializeRequest. Every deploy become reconnect event for every client wey connect.
  • Second replica no know sessions wey first replica get. Scaling out mean sticky routing for load balancer, or shared session store wey every replica read for every request.
  • Session table na memory wey dey grow with idle clients. DELETE no compulsory, 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 suppose understand before you touch any config.

Wetin every request dey carry now

Every POST wey go MCP endpoint dey stand on e own. Protocol version and client capabilities dey travel inside request body under _meta, while selected fields dey mirror for 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, e 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 validate header against body, so for those versions, header value no trustworthy.

Version disagreement don become normal per-request error instead of failed handshake. If server no implement the version wey request ask for, e answer 400 Bad Request with error -32022, UnsupportedProtocolVersion, and e 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 travel with every HTTP call, and system dey validate am each time. Details dey for authentication section below.

Cursors gats 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, na common thing to keep offset for memory and key am with session. Without session, cursor gats contain 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 dey share. Invalid cursor suppose return -32602. Sign am because opaque cursor still na client-supplied input wey your code decode and trust.

Subscriptions belong to request, no be connection. Client wey want change notifications dey send subscriptions/listen with filter wey name the types e want: toolsListChanged, promptsListChanged, resourcesListChanged and resourceSubscriptions. Server reply with notifications/subscriptions/acknowledged and keep that response stream open. If stream drop, server no keep anything, and client send subscriptions/listen again to get am back.

Application state wey cross calls becomes explicit handle. When server truly gats 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, dem fit log am, 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 takes query and returns results, with nothing for next call to resume and no reason to care which replica answer.

Deployment: reverse proxy, timeouts, health checks

The MCP endpoint na one path wey dey accept POST. Most traffic na short request and JSON response, wey any proxy fit handle. The exception na streaming response, where proxy defaults fit work against you. Na this part dey 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 dey hold SSE events until buffer fill or response end. The 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 quiet pass that time, nginx go close am, no be your server. Your logs go show healthy process, but your client go see dropped stream. Raise am for MCP location only, no be for the whole server. Servers also dey encouraged to send SSE comment line (line wey start with colon) as keep-alive during quiet periods. This one fit stop intermediaries from timing out the stream at all.

Caddy need less configuration. E dey buffer partially by default for wire efficiency, and e dey flush immediately when response carry Content-Type: text/event-stream. So streaming go 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 point to. No aim active check at MCP endpoint with GET, because server wey implement only this revision go answer 405 Method Not Allowed to GET and DELETE, while Caddy default health method na GET. The proxy go then mark backend wey dey perfectly healthy as down. Serve plain path like /healthz for proxy, and 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 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 dem, allow open POSTs finish, start new process, and make clients re-issue 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 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 drop, and client fit treat am 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 behaviour.

Authentication change wey happen when session no dey

When session dey, e easy to authenticate once for initialize, then use session ID as proof for everything wey follow. Session ID wey dem use like that na bearer credential wey no get audience, expiry, or revocation path, and na your own server mint am. When you remove sessions, that shortcut no dey again, so the replacement get stricter rules.

Protected MCP server dey act as OAuth 2.1 resource server. Every HTTP request from 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 no suppose accept or pass on tokens wey dem mean for anything else. Client go request the correct audience by sending resource parameter with the server canonical URI.

Discovery dey start from challenge. When request arrive 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 get permissions wey no reach enough go receive 403 Forbidden with error="insufficient_scope" and the scopes wey that operation need.

This one get two effects 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 time and key am by the token. Also, because no session dey hold identity, authorization must come from the token for every call. This one dey more honest than the session model, and e match the wider practice of keeping credentials out of the agent process, wey how to keep secrets out of an AI agent cover.

Wetín true for this revision, and wetín 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 older still dey use the handshake model. The specification call those revisions legacy, and e call the revisions wey use per-request metadata 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 fit resume. Dual-era server fit serve both for one endpoint: request wey carry modern _meta go process statelessly, 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 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 meaning if the library wey e dey use 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, not the protocol in general.

FAQ

Stateless MCP server mean say I no fit store anything?

No. Stateless describe the protocol, e no describe your application. Databases, queues and caches still work exactly as before. The change be say state wey dey span several calls must use an explicit identifier wey client go send 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 be for ordinary 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, wey be one open connection to one process. E go end when that process end, and client go send subscriptions/listen again to establish am again. Na connection lifetime be this, no be session affinity, and no routing rule dey prevent 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, and e suppose ignore an 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 one standalone GET stream. Servers wey must continue serving 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 one plain HTTP path wey your application dey serve, because a GET to the MCP endpoint correctly returns 405 and go mark healthy backend as down. Then check the protocol itself by sending POST to server/discover. Every 2026-07-28 server must implement am. Confirm say the reply na HTTP 200 and e list one protocol version wey your clients dey use. A 404 with JSON-RPC error -32601 mean say the process dey run but e no dey serve that method. A 400 with -32022 mean say the version wey you request no dey supported by that build.