Register

Stateless MCP: changes in 2026-07-28

Stateless MCP is the request model of the Model Context Protocol introduced in specification version 2026-07-28: every request carries all the information the server needs to process it, and the server does not keep a protocol session between requests. The protocol version, the client's identity and its capabilities travel in the _meta field of each request instead of being negotiated once in a handshake.

The change was made by a group of Specification Enhancement Proposals (SEPs), mainly SEP-2575, SEP-2567 and SEP-2322, and was released on 28 July 2026. The maintainers' announcement describes it as a move from a bidirectional, stateful protocol to a stateless request and response protocol. The overview of MCP and a short summary of the release are in the article What is MCP; this article covers each change and the migration from version 2025-11-25. HTTP-level details are in the article MCP transports.

Overview

Property Value
Introduced in Specification 2026-07-28, released 28 July 2026
Replaces initialize handshake and protocol sessions of 2025-11-25 and earlier
Main SEPs SEP-2575, SEP-2567, SEP-2322
Per-request metadata _meta keys with the io.modelcontextprotocol/ prefix
Discovery server/discover: mandatory for servers, optional for clients
Input from the client Multi Round-Trip Requests (MRTR)
Change notifications subscriptions/listen stream, opt-in per notification type
Compatibility with 2025-11-25 Not wire-compatible; dual-era implementations support both

The problem it solves

Up to version 2025-11-25 a client started with the initialize request and the notifications/initialized notification. The handshake fixed the protocol version and the capabilities of both sides for the rest of the connection. On Streamable HTTP the server could also assign a session with the Mcp-Session-Id header.

SEP-2575 names three consequences. A round-robin load balancer cannot be used, because the next request may reach an instance without the session, so operators need sticky sessions. When an instance fails, its sessions are lost and clients repeat the handshake.

SEP-2567 adds observations about real clients. Some hosts open a new session for every tool call, some keep one for the lifetime of the application, and almost none resume a session after a restart. A server author cannot predict what a session corresponds to. Because tools/list could differ between sessions, clients could not reuse a cached list.

Requests from the server to the client had a similar cost. To ask the user a question during a tool call, the server held a Server-Sent Events stream open, and the answer could arrive at another instance. According to SEP-2322, this blocked or limited the use of elicitation, sampling and roots by remote servers.

History

The table lists the steps from the first proposal to the end of the deprecation window.

Date Event
18 June 2025 SEP-2575 "Make MCP Stateless" is created
25 November 2025 Specification 2025-11-25, the last revision with the initialize handshake
19 December 2025 Transport Working Group maintainers publish "Exploring the Future of MCP Transports"
3 February 2026 SEP-2322 "Multi Round-Trip Requests" is created
11 March 2026 SEP-2567 "Sessionless MCP via Explicit State Handles" is created
28 July 2026 Specification 2026-07-28. The TypeScript, Python, Go and C# SDKs support it, the Rust SDK in beta
On or after 28 July 2027 First revision in which roots, sampling, logging and Dynamic Client Registration may be removed

SEPs of the release

The table lists the SEPs that define the stateless model and its direct consequences.

SEP Title What it changes
SEP-2575 Make MCP Stateless Removes initialize; adds _meta fields, server/discover, subscriptions/listen
SEP-2567 Sessionless MCP via Explicit State Handles Removes sessions and Mcp-Session-Id
SEP-2322 Multi Round-Trip Requests Replaces server requests with input_required results
SEP-2549 TTL for List Results Adds ttlMs and cacheScope
SEP-2243 HTTP Header Standardization for Streamable HTTP Transport Adds Mcp-Method and Mcp-Name
SEP-2663 Tasks Extension Moves tasks out of the core
SEP-2577 Deprecate Roots, Sampling, and Logging Deprecates three features
SEP-2596 Specification Feature Lifecycle and Deprecation Policy Defines feature states

What was removed

Handshake and session

The initialize request, the notifications/initialized notification and the Mcp-Session-Id header no longer exist. The first message of a client can be any request. The specification states the consequence: an open connection, including a stdio process, is not a conversation or a session. A client may interleave unrelated requests on one connection, and a server must not infer anything from earlier requests.

Removed methods and mechanisms

Several mechanisms existed because of the session or because the server could send requests:

  • ping is removed in both directions.
  • logging/setLevel is removed; the log level is set per request in _meta.
  • resources/subscribe, resources/unsubscribe and the HTTP GET stream are replaced by subscriptions/listen.
  • notifications/roots/list_changed is removed.
  • Stream resumability with Last-Event-ID is removed. After a broken response stream the client sends the request again with a new ID.
  • notifications/elicitation/complete and the elicitationId field of 2025-11-25 are removed.

Servers must not initiate JSON-RPC requests at all.

Request metadata

Each request carries reserved keys in params._meta. The table lists them together with the related keys of results and notifications.

Key Where Required Meaning
io.modelcontextprotocol/protocolVersion Request Yes Protocol version of this request
io.modelcontextprotocol/clientCapabilities Request Yes Client capabilities for this request; {} means none
io.modelcontextprotocol/clientInfo Request Recommended Client name and version
io.modelcontextprotocol/logLevel Request No, deprecated Minimum log level for this request
progressToken Request No Opts into progress notifications
traceparent, tracestate, baggage Any _meta No OpenTelemetry trace context
io.modelcontextprotocol/serverInfo Result Recommended Server name and version
io.modelcontextprotocol/subscriptionId Notification On listen streams ID of the subscriptions/listen request
JSON
{
  "jsonrpc": "2.0",
  "id": 7,
  "method": "resources/read",
  "params": {
    "uri": "file:///project/README.md",
    "_meta": {
      "io.modelcontextprotocol/protocolVersion": "2026-07-28",
      "io.modelcontextprotocol/clientInfo": { "name": "example-client", "version": "1.0.0" },
      "io.modelcontextprotocol/clientCapabilities": { "elicitation": { "form": {} } },
      "progressToken": "read-7",
      "traceparent": "00-0af7651916cd43dd8448eb211c80319c-00f067aa0ba902b7-01"
    }
  }
}

A request without a required key is rejected with error -32602 (Invalid params), on HTTP with status 400 Bad Request. The server must not take capabilities from earlier requests. If processing needs a capability the request did not declare, the server returns MissingRequiredClientCapabilityError with code -32021:

JSON
{
  "jsonrpc": "2.0",
  "id": 8,
  "error": {
    "code": -32021,
    "message": "Missing required client capability",
    "data": { "requiredCapabilities": { "elicitation": {} } }
  }
}

The clientInfo and serverInfo values are not verified. The specification intends them for display, logging and debugging, not for security decisions.

Discovery and version selection

server/discover returns the supported protocol versions, the capabilities and the identity of the server. Every server must implement it. A client may call it first or send any request directly and handle a version error. The request has no parameters except _meta:

JSON
{
  "jsonrpc": "2.0",
  "id": "discover-1",
  "method": "server/discover",
  "params": {
    "_meta": {
      "io.modelcontextprotocol/protocolVersion": "2026-07-28",
      "io.modelcontextprotocol/clientInfo": { "name": "example-client", "version": "1.0.0" },
      "io.modelcontextprotocol/clientCapabilities": {}
    }
  }
}

The response can carry instructions, a text for the model about using the server, and the supported extensions:

JSON
{
  "jsonrpc": "2.0",
  "id": "discover-1",
  "result": {
    "resultType": "complete",
    "supportedVersions": ["2026-07-28", "2025-11-25"],
    "capabilities": {
      "tools": { "listChanged": true },
      "prompts": {},
      "extensions": { "io.modelcontextprotocol/tasks": {} }
    },
    "instructions": "Issue tracker server. Search issues before creating new ones.",
    "_meta": {
      "io.modelcontextprotocol/serverInfo": { "name": "example-tracker", "version": "2.4.0" }
    },
    "ttlMs": 3600000,
    "cacheScope": "public"
  }
}

For an unknown or unsupported version the server returns UnsupportedProtocolVersionError with code -32022. The client picks a version from data.supported and retries:

JSON
{
  "jsonrpc": "2.0",
  "id": 1,
  "error": {
    "code": -32022,
    "message": "Unsupported protocol version",
    "data": { "supported": ["2026-07-28", "2025-11-25"], "requested": "1900-01-01" }
  }
}

Multi Round-Trip Requests

How the exchange works

Multi Round-Trip Requests (MRTR) let a server obtain input from the client without sending a request of its own. Only tools/call, resources/read and prompts/get may receive an interim result.

  1. The client sends a request, for example tools/call.
  2. The server needs more information and returns resultType: "input_required".
  3. The client fulfils the listed requests, for example by asking the user.
  4. The client repeats the original request with a new JSON-RPC id, the answers and the server's state.
  5. Any server instance processes the retry and returns the final result or another input_required.

Fields

Every result carries resultType: "complete" for a final result, "input_required" for an interim one. Extensions may add values. A client treats an unknown value as invalid and an absent field, sent by older servers, as "complete".

An interim result contains inputRequests, requestState or both. inputRequests is a map whose keys are chosen by the server and whose values are elicitation/create, sampling/createMessage or roots/list requests. requestState is an opaque string that the client must return unchanged and must not parse. The answers go into inputResponses under the same keys. An input_required result is shown in What is MCP; the retry looks like this:

JSON
{
  "jsonrpc": "2.0",
  "id": 2,
  "method": "tools/call",
  "params": {
    "name": "create_repository",
    "arguments": { "name": "demo" },
    "inputResponses": {
      "github_login": { "action": "accept", "content": { "name": "octocat" } }
    },
    "requestState": "eyJzdGVwIjoyfQ.signature",
    "_meta": {
      "io.modelcontextprotocol/protocolVersion": "2026-07-28",
      "io.modelcontextprotocol/clientCapabilities": { "elicitation": { "form": {} } }
    }
  }
}

The server may ask only for what the request declared: without the elicitation capability, inputRequests must not contain elicitation/create. If answers are missing, the server should send a new input_required rather than an error. Results of retries are not cached.

State without server storage

A result may carry only requestState. SEP-2322 describes load shedding: an overloaded instance stops processing and returns the accumulated state, and the client's immediate retry continues on another instance.

JSON
{
  "jsonrpc": "2.0",
  "id": 5,
  "result": {
    "resultType": "input_required",
    "requestState": "gAAAAABn-checkpoint-42"
  }
}

Integrity of the request state

The state passes through the client, so the server must treat it as input controlled by an attacker. If it affects authorization, access to resources or business logic, the server must protect its integrity, for example with HMAC or authenticated encryption (AEAD), and reject state that fails verification. Against replay, the protected payload should contain the authenticated principal, a short expiry and an identifier of the original request. These measures do not make state single-use; one-time redemption is enforced on the server side. Other attacks are covered in MCP security.

Explicit state handles

State that spans several calls is referenced by an explicit identifier passed in each request. For application state the specification describes a tool design pattern, not a protocol element. A state handle is an identifier that a creation tool returns and that the model passes back as an argument of later calls:

JSON
{
  "jsonrpc": "2.0",
  "id": 11,
  "result": {
    "resultType": "complete",
    "content": [{ "type": "text", "text": "Created basket bsk_a1b2c3" }],
    "structuredContent": { "basket_id": "bsk_a1b2c3" }
  }
}

The next call, add_item, takes { "basket_id": "bsk_a1b2c3", "sku": "shoes" } as arguments. The server stores the basket under that key. The specification gives guidance on handle design:

  • Authorization. On an authenticated server a handle is a name, and the server checks the caller against it on every call. Without authentication it acts as a bearer token and needs enough entropy, for example a UUIDv4, and a limited lifetime.
  • Opacity. A handle with visible internal structure invites parsing or guessing.
  • Lifetime. The retention policy is written in the description of the creation tool, where the model sees it.
  • Expiry errors. A call with an expired handle returns a tool execution error that says so.

Lists follow the same rule. tools/list may change over time and may depend on the authorization of the request, but not on the connection or on other requests. A tool that used to appear after connect_database is listed from the start and takes a connection_id argument. Tool design is covered in MCP tools.

Subscriptions and notifications

Notifications not tied to one request arrive on a stream opened by subscriptions/listen. The client lists the types it wants, and the server must not send other types:

JSON
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "subscriptions/listen",
  "params": {
    "notifications": {
      "toolsListChanged": true,
      "resourceSubscriptions": ["file:///project/config.json"]
    },
    "_meta": {
      "io.modelcontextprotocol/protocolVersion": "2026-07-28",
      "io.modelcontextprotocol/clientCapabilities": {}
    }
  }
}

The filter fields are toolsListChanged, promptsListChanged, resourcesListChanged and resourceSubscriptions. The first message on the stream must be notifications/subscriptions/acknowledged with the subset the server agreed to deliver:

JSON
{
  "jsonrpc": "2.0",
  "method": "notifications/subscriptions/acknowledged",
  "params": {
    "notifications": { "toolsListChanged": true },
    "_meta": { "io.modelcontextprotocol/subscriptionId": 1 }
  }
}

Every notification on the stream carries io.modelcontextprotocol/subscriptionId, equal to the ID of the listen request, so several subscriptions can share a channel. The server keeps no subscription state across reconnections, and the client sends the request again after a lost channel. Notifications scoped to one request, such as notifications/progress, still arrive on the response stream of that request.

Caching hints

Results of server/discover, tools/list, prompts/list, resources/list, resources/templates/list and resources/read must carry two fields. ttlMs is how many milliseconds the result stays fresh; 0 means stale at once. cacheScope is "public" when the result contains no user data and may be shared by gateways, or "private" when it may be reused only within the same authorization context.

JSON
{
  "jsonrpc": "2.0",
  "id": 3,
  "result": {
    "resultType": "complete",
    "tools": [
      { "name": "search_issues", "description": "Search issues by text", "inputSchema": { "type": "object" } }
    ],
    "nextCursor": "page-2",
    "ttlMs": 300000,
    "cacheScope": "private"
  }
}

The cache key is the method with the parameters that affect the result, such as uri or cursor. Each page is cached separately. TTL is a freshness hint, not a polling interval, and a change notification invalidates a cached result immediately. Servers should return tools in a deterministic order, which the specification links to client caching and to the model's prompt cache.

Deprecations and the feature lifecycle

SEP-2596 defines three feature states. An Active feature is part of the current revision. A Deprecated feature remains with a documented migration path, and new implementations should not adopt it. A Removed feature is absent from the next revision. At least twelve months pass between deprecation and eligibility for removal, counted from the release of the deprecating revision; for an active security risk the window can be shortened to ninety days.

The table lists the features deprecated in 2026-07-28.

Feature Migration path Earliest removal
Roots Tool parameters, resource URIs or server configuration First revision on or after 2027-07-28
Sampling Direct calls to model provider APIs First revision on or after 2027-07-28
Logging stderr for stdio, OpenTelemetry First revision on or after 2027-07-28
Dynamic Client Registration Client ID Metadata Documents First revision on or after 2027-07-28

SEP-2577 cites low adoption, vague semantics of roots, the complexity of sampling and existing logging tools. The registry also lists the HTTP+SSE transport, deprecated since 2025-03-26 and reclassified under the new policy. During the window the wire format does not change. Tier 1 SDKs must mark deprecated APIs with the native mechanism of their language.

Compatibility between versions

The specification calls versions with per-request metadata (2026-07-28 and later) modern and versions with the initialize handshake (2025-11-25 and earlier) legacy. An implementation that supports both is dual-era. The table shows each combination.

Client Server Outcome
Modern Modern Works; version mismatches are resolved by retry
Modern Legacy Fails; the client shows an error
Dual-era Modern Works in modern mode
Dual-era Legacy Works after fallback to initialize
Legacy Modern Fails; legacy clients cannot move forward
Legacy Dual-era Works under the legacy revision

A dual-era client detects the era by the transport. On stdio it sends server/discover first: a DiscoverResult or a recognized modern error means a modern server, any other error or no answer means a legacy one. On HTTP it sends a modern request and inspects the body of a 400 Bad Request before falling back. The client should cache the result for the server process or origin.

A dual-era server chooses the mode by the first message: modern _meta means stateless processing, initialize selects legacy semantics. A modern-only server should list its supported versions in the error it returns to initialize.

Migration from 2025-11-25

The announcement expects most of the cost for applications that depended on session identifiers. SEP-2567 cites an automated survey of 1000 open source servers: 90% had no application-level reference to the session ID, 2.5% kept application state keyed by it.

Servers

  1. Implement server/discover with versions, capabilities, serverInfo and caching hints.
  2. Read the version and capabilities from _meta of every request; return -32602, -32022 or -32021 as described above.
  3. Add resultType to every result.
  4. Stop issuing Mcp-Session-Id. Move session-keyed state to state handles or to the authenticated principal.
  5. Make lists independent of the connection, order them deterministically and add ttlMs and cacheScope.
  6. Replace requests to the client with input_required results and protect requestState.
  7. Serve subscriptions/listen instead of the GET stream and resources/subscribe.
  8. Move experimental tasks to the io.modelcontextprotocol/tasks extension.
  9. Keep the initialize path if legacy clients must still work.

Clients

  1. Add protocolVersion, clientCapabilities and clientInfo to _meta of every request.
  2. Handle -32022 by retrying with a supported version, or call server/discover in advance.
  3. Dispatch on resultType and implement the MRTR loop with a new id and an unchanged requestState.
  4. Open subscriptions/listen for change notifications and reopen it after a lost channel.
  5. Cache lists by ttlMs and cacheScope and invalidate them on notifications.
  6. Repeat a request with a new ID when its response stream breaks.
  7. Stop sending ping, logging/setLevel and Mcp-Session-Id.
  8. Keep state handles in the context when the conversation is compacted.

Checks of contract changes between versions are described in regression testing of an MCP server.

State in a stateless protocol

Statelessness applies to the protocol, not to applications. SEP-2575 compares it with HTTP: applications keep state, and the request carries the state or a reference to it. The table compares these references with the removed session.

Mechanism Where the state lives Lifetime Who carries the reference
Protocol session, removed Server, under Mcp-Session-Id Set by the host, undefined Transport header
State handle Server storage Policy in the tool description Model, as a tool argument
requestState Inside the string One logical request Client, echoed unchanged
Task ID Server, durable ttlMs of the task Client, in tasks/get
Subscription Open stream Until cancellation or disconnect ID of the listen request

A session gave one scope per connection. Handles give as many scopes as the model creates, and each can be shared or isolated: according to SEP-2567, subagents can share one basket and use separate browser contexts. Task IDs belong to the Tasks extension, described in MCP extensions.

Terms

Term Meaning
Stateless protocol Protocol in which each request contains everything needed to process it
server/discover Mandatory server method that returns supported versions, capabilities and server identity
resultType Result field that marks a result as final, interim or of an extension type
Request state Opaque string with the server's progress that the client returns unchanged on retry
State handle Identifier returned by a tool and passed back by the model to refer to server-side state
Caching hints ttlMs and cacheScope fields that tell how long and by whom a result may be cached
Dual-era implementation Client or server that supports both 2026-07-28 and handshake-based versions
Deprecation window At least twelve months between deprecation of a feature and its eligibility for removal

Sources

Series

Model Context Protocol (MCP)
  1. What is MCP (Model Context Protocol)
  2. Tools in MCP
  3. Resources and resource templates in MCP
  4. Prompts in MCP
  5. MCP transports: stdio and Streamable HTTP
  6. Stateless MCP: changes in 2026-07-28Current
  7. Authorization in MCP
  8. MCP security
  9. MCP extensions
  10. MCP server over an existing API
  11. Testing MCP servers: the basics
  12. Evaluating an MCP server with models
  13. Regression testing of an MCP server
  14. Publishing an MCP server to the MCP Registry

Wiki articles are written and maintained by the ComplexQA team.