MCP 2026-07-28: The Protocol Finally Becomes Stateless

The new MCP specification removes sessions and handshakes while adding extensions, tasks, MCP Apps, caching and OpenTelemetry conventions.

15 min read
  • #AI Engineering
  • #Mcp
  • #Llm
  • #Software Architecture

Public discussion around MCP usually focuses on tools, resources and prompts: what a model can do through an MCP server. The most important change in the new specification is much less spectacular. MCP is removing protocol sessions.

With revision 2026-07-28, the initialize handshake, the subsequent initialized notification and the Mcp-Session-Id header disappear. Instead of negotiating capabilities and protocol version once at the beginning of a connection, every request now contains all information the server needs to process it.

That initially sounds like a minor implementation detail. In practice, it is the step that turns MCP from a convenient protocol for local tools into a much better foundation for distributed and horizontally scalable AI systems.

The maintainers describe 2026-07-28 as the largest revision since MCP was introduced. Alongside the stateless protocol core, it adds an official extension model, Multi Round-Trip Requests, MCP Apps, the new Tasks extension, caching metadata, OpenTelemetry conventions and several OAuth changes. Roots, Sampling and Logging are marked deprecated at the same time.

It is not called MCP 2.0

MCP does not use semantic versioning for protocol revisions. It uses dates. The new revision is therefore:

2026-07-28

Names such as “MCP 2.0” are misleading. Version 2 of the official TypeScript and Python SDKs now exists, but SDK versions are independent from protocol versions.

That distinction matters particularly in TypeScript. Updating to the v2 packages does not automatically enable the new wire protocol. Support for 2026-07-28 has to be enabled explicitly through the new server and client entry points. Existing applications can therefore update their SDK without simultaneously changing communication with every client.

The problem with the old sessions

Under the previous specification, 2025-11-25, a connection began with an initialize request. The client used it to tell the server its protocol version, capabilities and some information about itself.

The server responded with, among other things, an Mcp-Session-Id:

Client
  |
  | initialize
  v
MCP-Server A
  |
  | Mcp-Session-Id: 1868a90c-3a3f-4f5b
  v
Client

Every subsequent request had to include that session ID:

POST /mcp HTTP/1.1
Mcp-Session-Id: 1868a90c-3a3f-4f5b
Content-Type: application/json

{
  "jsonrpc": "2.0",
  "id": 2,
  "method": "tools/call",
  "params": {
    "name": "search",
    "arguments": {
      "query": "stateless MCP"
    }
  }
}

For a local MCP server started through stdio, this is barely a problem. There is exactly one process and one client. Once an MCP server is exposed over HTTP and scaled horizontally, however, the usual problems of stateful systems appear.

Either the load balancer needs sticky sessions so that a client repeatedly reaches the same instance, or every instance has to place session state in Redis, a database or another shared store.

An MCP deployment can therefore quickly end up looking like this:

                         +----------------+
                    +--->| MCP-Server A   |
                    |    +----------------+
Client -> Load Balancer -+                 +--> shared session store
                    |    +----------------+
                    +--->| MCP-Server B   |
                         +----------------+

For a protocol whose purpose is primarily to standardise tools and data sources, that is a substantial amount of infrastructure baggage.

Every request is now complete

With 2026-07-28, every request is processed independently. Protocol version and client capabilities live in _meta. Over HTTP, the protocol version is additionally included in the MCP-Protocol-Version header.

A tool call now looks schematically like this:

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

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "search",
    "arguments": {
      "query": "stateless MCP"
    },
    "_meta": {
      "io.modelcontextprotocol/protocolVersion": "2026-07-28",
      "io.modelcontextprotocol/clientCapabilities": {},
      "io.modelcontextprotocol/clientInfo": {
        "name": "my-client",
        "version": "1.0.0"
      }
    }
  }
}

protocolVersion and clientCapabilities have to be present on every request. clientInfo is recommended but optional.

That means every request can land on any instance:

                         +----------------+
                    +--->| MCP-Server A   |
                    |
Client -> Load Balancer -+---> MCP-Server B
                    |
                    +--->| MCP-Server C   |
                         +----------------+

At the protocol layer there is no longer a session, no shared session store and no reason for sticky sessions. The server may not even assume that two requests on the same connection belong to the same conversation or agent run.

Version discovery without a handshake

The new MCP still needs a way to establish compatible versions. Instead of a mandatory handshake, it now provides the server/discover RPC call.

A server has to expose, among other things, its supported protocol versions and capabilities. A client may call server/discover before its first real request, but it does not have to. It can instead send a request immediately using its preferred version.

If the server does not support that version, it responds with the new error code -32022:

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

The client can then choose a shared version and retry the request. Modern clients can therefore talk to both new and old servers without requiring every server to migrate on the same day. The specification explicitly distinguishes modern, legacy and so-called Dual-Era implementations.

Stateless does not mean applications cannot have state

Removing the MCP session does not mean every tool call has to be completely isolated. State is simply no longer tied implicitly to a connection.

A browser tool, for example, can still expose several related calls:

create_browser()
    -> browser_id: "browser_7d91"

navigate(
    browser_id = "browser_7d91",
    url = "https://example.com"
)

take_screenshot(
    browser_id = "browser_7d91"
)

close_browser(
    browser_id = "browser_7d91"
)

The same pattern works for shopping carts, database transactions, sandbox environments, search indexes or temporary working directories.

The difference is that state becomes explicit. The model sees the identifier, can use it in later tool calls and can even manage several instances in parallel. The server may continue to store the actual state internally. It simply must not bind that state to a particular HTTP connection or MCP process.

Such a handle should ideally:

  • be opaque and reveal no internal information
  • be bound to the authenticated user
  • have a limited lifetime
  • become invalid reliably after expiry or completion
  • not serve as the sole authorisation proof for write operations

The new architecture therefore does not eliminate state. It forces developers to model state deliberately as part of the application. That is initially more work, but produces interfaces that are easier to test and scale.

Multi Round-Trip Requests replace the backchannel

Previously, an MCP server could send its own JSON-RPC request to the client while processing another request. This was used for Elicitation, Sampling or roots/list, for example.

That direction no longer exists in the new revision. A server may not initiate its own JSON-RPC requests. If it needs additional information, it responds with an InputRequiredResult.

A tool that requires confirmation before deleting files could respond like this:

{
  "jsonrpc": "2.0",
  "id": 42,
  "result": {
    "resultType": "input_required",
    "inputRequests": {
      "confirm_deletion": {
        "method": "elicitation/create",
        "params": {
          "mode": "form",
          "message": "Sollen die drei ausgewählten Dateien gelöscht werden?",
          "requestedSchema": {
            "type": "object",
            "properties": {
              "confirmed": {
                "type": "boolean"
              }
            },
            "required": [
              "confirmed"
            ]
          }
        }
      }
    },
    "requestState": "AEAD-protected-state"
  }
}

The client gathers the required input and then repeats the original tool call, sending back the inputResponses and the unchanged requestState.

The second call is not a continuation of the same connection. It is a new request with a new JSON-RPC ID. Any server instance can process it as long as the request contains all information required to do so.

requestState is an interesting detail. The client is supposed neither to interpret nor modify it. From the server’s perspective, it is still untrusted input because it returns through the client.

As soon as requestState influences authorisation decisions or business logic, the server has to protect its integrity. The specification suggests HMAC or AEAD and recommends binding at least:

  • the authenticated user
  • a short expiry time
  • the original RPC call
  • a hash of relevant parameters

For genuine one-time actions, a signed token alone is not sufficient. The server still has to track whether the state has already been consumed.

Architecturally, Multi Round-Trip Requests are explicit resumable state machines. They are less convenient than a permanently open bidirectional channel, but considerably easier to distribute across ordinary HTTP infrastructure.

Routable: gateways no longer need to parse JSON

Streamable HTTP now requires standardised headers such as Mcp-Method and Mcp-Name.

A gateway can therefore determine from headers alone whether a request is, for example, tools/list, tools/call or resources/read. It no longer needs to parse the JSON-RPC body first.

This enables configurations such as:

Mcp-Method = tools/list
    -> high cache hit rate
    -> generous rate limit

Mcp-Method = tools/call
Mcp-Name   = delete_document
    -> separate rate limit
    -> additional audit logging

Mcp-Method = resources/read
    -> different backend pool

Servers have to verify that the headers and JSON-RPC body agree. A client therefore cannot claim Mcp-Name: harmless_search in the header while invoking another tool in the body.

These headers are not a security boundary, however. Authorisation still has to use the authenticated user, actual method and validated request data. The headers are primarily intended for routing, rate limiting, metrics and traffic analysis.

Cacheable: tool and resource lists get a lifetime

Responses from tools/list, prompts/list, resources/list, resources/read and resources/templates/list now include ttlMs and cacheScope.

A response can look like this:

{
  "resultType": "complete",
  "tools": [
    {
      "name": "search_documents",
      "description": "Durchsucht das Dokumentenarchiv"
    }
  ],
  "ttlMs": 300000,
  "cacheScope": "private"
}

ttlMs specifies how long the answer may be considered current. cacheScope determines whether it may be cached across users:

public
    The response may be cached by shared intermediaries.

private
    The response belongs to the current user or authorisation context.

Clients often request tools/list repeatedly. That can transfer substantial tool schemas and then place them into the model context again. Sensible caching reduces network traffic and can simultaneously improve prompt-cache hit rates.

The specification additionally recommends a deterministic tool order. A server should not return the same tools in a different order on every request.

cacheScope: public still needs to be used carefully. As soon as a tool or resource list depends on roles, tenants, feature flags or user permissions, it is private. A wrongly configured scope can otherwise expose information about functions or resources belonging to other users.

Traceable: OpenTelemetry becomes part of the convention

MCP now defines propagation of W3C Trace Context through _meta, including the familiar fields:

{
  "_meta": {
    "traceparent": "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01",
    "tracestate": "vendor=value",
    "baggage": "tenant=example"
  }
}

A trace can therefore travel from the host through the MCP client and MCP server to downstream APIs, databases or model calls.

For production agent systems, that is more important than it first appears. Without an end-to-end trace, all you often know is that an answer took a long time. With one common trace, you can distinguish time spent in the model, the MCP transport, tool execution or an external service.

The specification standardises only the field names and propagation. Which data may actually be placed in baggage remains a security decision. Credentials, complete prompts and personal data do not belong there.

Subscriptions become a normal request

The previous HTTP GET connection and resources/subscribe / resources/unsubscribe calls are replaced by subscriptions/listen.

The client starts a normal POST request whose response remains open as a long-running stream. It can specify which events it wants, for example:

  • changes to the tool list
  • changes to prompts
  • changes to resources
  • updates to specific subscribed resources

Again, the state belongs to the concrete request rather than the underlying HTTP connection. If the connection breaks, the client starts a new subscriptions/listen request.

The previous resume mechanism through Last-Event-ID and SSE event IDs is gone. A normal request whose response stream breaks is also considered lost and has to be repeated with a new JSON-RPC ID.

That has an important consequence for tools with side effects: they should either be idempotent or provide their own idempotency key or duplicate detection. Otherwise a request that completed successfully but lost its response can perform the same action twice when retried.

This is not a new distributed-systems problem. MCP’s deliberate removal of transport sessions simply makes it more visible.

Extensions become an official part of MCP

MCP Extensions existed before, but without a fully defined lifecycle. The new revision specifies how extensions are named, negotiated and evolved.

Identifiers use reverse-DNS names:

{
  "capabilities": {
    "extensions": {
      "io.modelcontextprotocol/ui": {
        "mimeTypes": [
          "text/html;profile=mcp-app"
        ]
      },
      "io.modelcontextprotocol/tasks": {}
    }
  }
}

Extensions can be versioned independently from the core specification and maintained by their own maintainers. New capabilities no longer need to become part of the protocol core immediately.

The first two official extensions show what this model is intended for.

MCP Apps: a tool can bring its own UI

MCP Apps allow a server to provide interactive HTML interfaces alongside ordinary text or JSON results.

The host renders that interface inside an isolated iframe. A tool can therefore return, for example:

  • a dashboard with metrics
  • an editable table
  • a form with validation
  • a map view
  • a preview with an approval button
  • a multistep configuration dialog

UI templates are declared in the tool description itself. The host can preload, cache and inspect them for security properties. Actions from the UI then travel through MCP again and therefore through the same authorisation, consent and audit paths as normal tool calls.

This is a sensible design. Not every interaction maps well to natural language. For a 50-row table, date range or graphical selection, a small controlled interface is often more accurate and efficient than ten more chat messages.

Tasks for long-running operations

Tasks were still an experimental part of the protocol core in 2025-11-25. They now move into the official io.modelcontextprotocol/tasks extension and are adapted to the stateless model.

Instead of returning a finished result, a server may return a task handle from a tool call. The client manages the rest of the lifecycle through:

tasks/get
tasks/update
tasks/cancel

tasks/list has been removed. Without a protocol session, a global task list is difficult to associate safely with a user, client or agent run. The client therefore has to know the specific task handle.

This maps well to operations such as:

  • processing large document batches
  • longer code analyses
  • data exports
  • video or image processing
  • extensive research jobs
  • jobs running on external systems

Crucially, the client does not decide in advance whether a call should run as a task. The server makes that decision and returns a task handle when necessary.

OAuth and OpenID Connect become more precise

A substantial part of the revision concerns less visible but important authorisation details.

Clients have to validate any iss parameter in the Authorization Response against the expected issuer. That reduces the risk of so-called mix-up attacks where responses from different Authorization Servers are confused.

With Dynamic Client Registration, the client has to declare the correct OpenID Connect application_type. That prevents, among other things, a desktop or CLI client from being registered incorrectly as a web application and subsequently being unable to use its local redirect URI.

Stored client credentials also have to be bound to the issuer that created them. If a resource switches to another Authorization Server, existing credentials must not simply be reused.

Dynamic Client Registration itself is now deprecated in favour of Client ID Metadata Documents. It remains for compatibility, but new implementations are expected to follow the newer mechanism.

Roots, Sampling and Logging are deprecated

The new revision introduces a formal lifecycle for protocol features for the first time:

Active -> Deprecated -> Removed

At least twelve months have to pass between deprecation and possible removal. Existing functionality therefore does not vanish overnight.

With 2026-07-28, several familiar parts are affected:

FeatureRecommended replacement
RootsTool parameters, Resource URIs or server configuration
SamplingDirect integration with an LLM provider API
Loggingstderr for stdio or OpenTelemetry
HTTP+SSEStreamable HTTP
Dynamic Client RegistrationClient ID Metadata Documents

I will hardly miss Roots or Logging. Passing directories and files explicitly as tool parameters or Resource URIs is easier to reason about than an implicit set of Roots. OpenTelemetry is the better abstraction for production logging anyway.

I am more sceptical about Sampling. Sampling allowed an MCP server to ask the host for a model call. The host could centrally manage model access, cost controls, permissions and user interaction.

Direct integration with a provider API simplifies MCP itself, but it couples a server more tightly to concrete model providers and may force it to manage credentials and costs on its own. Sampling continues to work during the deprecation period and can travel through Multi Round-Trip Requests. For new systems, however, it is worth considering carefully whether an MCP server should really become an LLM client itself.

Full JSON Schema 2020-12 for tools

Tool schemas may now use the complete feature set of JSON Schema 2020-12.

That includes:

oneOf
anyOf
allOf
if / then / else
$ref
$defs

A tool can, for example, define two alternative ways to address an account:

{
  "type": "object",
  "oneOf": [
    {
      "properties": {
        "iban": {
          "type": "string"
        },
        "amount": {
          "type": "number",
          "exclusiveMinimum": 0
        }
      },
      "required": [
        "iban",
        "amount"
      ]
    },
    {
      "properties": {
        "accountId": {
          "type": "string"
        },
        "amount": {
          "type": "number",
          "exclusiveMinimum": 0
        }
      },
      "required": [
        "accountId",
        "amount"
      ]
    }
  ]
}

Output schemas no longer have to describe an object. Arrays, strings, numbers and other valid JSON values are allowed as well. structuredContent can therefore contain any JSON value.

The additional expressive power creates new risks, though. External $ref URLs must not be fetched automatically by default. Otherwise manipulated schemas could trigger access to internal networks or local services. Validators should also limit depth, number of subschemas and validation time so that a deliberately complex schema cannot become a denial-of-service attack.

A schema being allowed to be highly complex does not mean it should be. LLMs often handle simple, clearly named fields better than several nested oneOf and allOf constructions. The schema should be as complex as necessary and as simple as possible.

TypeScript v2 in practice

The official TypeScript SDK has been split into several packages. A server now uses @modelcontextprotocol/server.

The new HTTP entry point is createMcpHandler:

import {
  createMcpHandler,
  McpServer,
} from '@modelcontextprotocol/server';
import * as z from 'zod/v4';

const handler = createMcpHandler(() => {
  const server = new McpServer(
    {
      name: 'document-server',
      version: '1.0.0',
    },
    {
      capabilities: {
        tools: {},
      },
    },
  );

  server.registerTool(
    'search_documents',
    {
      description: 'Durchsucht das Dokumentenarchiv',
      inputSchema: z.object({
        query: z.string().min(1),
        limit: z.number().int().min(1).max(100).default(10),
      }),
    },
    async ({ query, limit }) => {
      const results = await searchDocuments(query, limit);

      return {
        content: [
          {
            type: 'text',
            text: JSON.stringify(results),
          },
        ],
      };
    },
  );

  return server;
});

export default handler;

createMcpHandler creates an appropriate server instance for every request and by default supports both the new revision and stateless requests from the 2025 protocol generation.

In a Node application, the Web-standard handler can be attached through toNodeHandler:

import { toNodeHandler } from '@modelcontextprotocol/node';

app.all('/mcp', toNodeHandler(handler));

A client enables automatic negotiation explicitly:

import { Client } from '@modelcontextprotocol/client';

const client = new Client(
  {
    name: 'document-client',
    version: '1.0.0',
  },
  {
    versionNegotiation: {
      mode: 'auto',
    },
  },
);

await client.connect(transport);

console.log(client.getProtocolEra());
// "modern" oder "legacy"

In auto mode, the client first attempts server/discover. If it detects an older server, it falls back to the previous initialize handshake.

To allow only the new protocol, the revision can be pinned:

const client = new Client(
  {
    name: 'document-client',
    version: '1.0.0',
  },
  {
    versionNegotiation: {
      mode: {
        pin: '2026-07-28',
      },
    },
  },
);

A connection to a pure 2025-11-25 server now fails instead of silently falling back to the older protocol.

What migration involves

There is no reason to rebuild a simple local MCP server immediately. Older clients and servers are not switched off on release day. The new version is an additional protocol revision, not a centrally enforced upgrade.

For an MCP server exposed over HTTP, I would migrate in this order:

  1. Update the SDK without changing the wire protocol immediately.
    This keeps API changes and package restructuring separate from the protocol migration.

  2. Introduce Dual-Era operation.
    The server should initially support both 2025-11-25 and 2026-07-28 so that clients can migrate gradually.

  3. Find session dependencies.
    That includes Mcp-Session-Id, sticky sessions, session-ID maps and state coupled to one process or connection.

  4. Replace application state with explicit handles.
    Handles need a clear lifetime, user binding and authorisation checks.

  5. Move server-initiated requests to MRTR.
    Elicitation, Sampling and Roots have to use InputRequiredResult, inputResponses and, where necessary, requestState.

  6. Review write tools for retry safety.
    Requests can be sent again after broken streams. Side effects need idempotency or duplicate detection.

  7. Set caching metadata deliberately.
    cacheScope in particular must not be set to public indiscriminately.

  8. Connect tracing end to end.
    traceparent, tracestate and baggage should propagate to downstream APIs.

  9. Review OAuth configuration.
    This includes issuer validation, credential binding, scope step-up and the future replacement of Dynamic Client Registration.

  10. Do not introduce deprecated features into new code.
    Existing Roots, Sampling and Logging implementations can continue to run. New functionality should already use the recommended alternatives.

My assessment

2026-07-28 is the most important MCP revision so far, even though many of its changes are barely visible to an end user.

MCP Apps are easier to demonstrate. An interactive table inside a chat is more tangible than a removed session header. For the protocol’s long-term success, the stateless core matters more.

The previous architecture worked well for local processes and simple integrations. For centrally operated MCP platforms with several instances, gateways, tenants and authorisation, it was unnecessarily complicated. The new revision aligns much more closely with the properties expected from ordinary HTTP services:

  • Requests can be routed independently.
  • State is referenced explicitly.
  • Responses can be cached under controlled rules.
  • Traces work across system boundaries.
  • Optional capabilities evolve as extensions.
  • Deprecated features have a predictable lifecycle.

The complexity does not disappear. A server that used to keep state in a session map now needs a proper handle model. A bidirectional interaction becomes an explicit state machine. Requests with side effects have to handle retries safely.

Those are application problems rather than transport problems. That is exactly where they belong.

With this revision, MCP feels less like a convenient JSON-RPC wrapper for local tools and more like a protocol on which distributed agent systems can actually be operated. An immediate upgrade is not necessary for local hobby projects. Anyone providing MCP servers as shared infrastructure should study 2026-07-28 closely.