Skip to content

필사 모드: MCP Drops Sessions — Reading the Stateless Core in the 2026-07-28 Revision

English
0%
정확도 0%
💡 왼쪽 원문을 읽으면서 오른쪽에 따라 써보세요. Tab 키로 힌트를 받을 수 있습니다.

Introduction — Let Me State This Post's Shelf Life Up Front

Today is July 16, 2026. And MCP's Current specification is still 2025-11-25 — that is what the official versioning document says.

The 2026-07-28 revision discussed here is still a draft, locked as a release candidate (RC). Per the RC announcement, the RC locked on May 21, 2026, and the final spec is scheduled to publish on July 28, 2026 — 12 days from when this post was written.

So this is not "an explainer of the new spec" but "a preview read of the RC." Details can still change, and if they do, parts of this post will turn out wrong. Read it with that caveat. What MCP is in the first place, and what tools, resources, and prompts look like, is already covered in the MCP reference post, so here I focus only on what is changing.

What Was the Problem — Sessions Fighting the Load Balancer

After Streamable HTTP opened the door to remote deployment, the problems showed up at scale. The 2026 roadmap puts it bluntly — stateful sessions fight load balancers, horizontal scaling needs workarounds, and there is no standard way for a registry or crawler to learn what a server does without connecting to it.

Concretely, this is what it meant. In the existing design, a client first does an initialize handshake, the server issues an Mcp-Session-Id, and every subsequent request is tied to that session. Operationally, that drags along:

  • Sticky sessions. Requests in the same session must go to the same instance, so round robin is out.
  • Shared session storage. Running multiple instances means sharing session state somewhere.
  • Deep packet inspection at the gateway. Telling whether a request is tools/call or tools/list means cracking open the JSON-RPC body — at the HTTP layer they are all identical POSTs.

The RC announcement sums up the payoff of this change in one line — a remote MCP server that used to need sticky sessions, shared session storage, and deep packet inspection at the gateway can now run behind a plain round-robin load balancer.

The roadmap also attaches an honest caveat — this cycle does not add new official transports, it only evolves the existing ones. This is not a new architecture bolted on; it is a reworking of the HTTP transport.

The Stateless Core — the Handshake and the Session Disappear

The top of the major changes listed in the official changelog is all about this.

SEP-2575: removes the initialize and notifications/initialized handshake. Instead, every request carries its own protocol version and client capabilities in _metaio.modelcontextprotocol/protocolVersion, io.modelcontextprotocol/clientCapabilities. Clients are recommended to identify themselves on every request (io.modelcontextprotocol/clientInfo), and servers identify themselves in the _meta of each result. A version mismatch returns UnsupportedProtocolVersionError.

SEP-2567: removes the Mcp-Session-Id header and the session itself from the protocol layer. So list endpoints (tools/list, resources/list, prompts/list) no longer vary per connection. How, then, does a server that needs state between calls handle it? It passes a server-issued, explicit handle back and forth as an ordinary tool argument. That is the central philosophy here — not banning state, but pulling state down from the protocol's implicit layer into the application's explicit data.

Addition of server/discover: servers MUST implement this RPC. It advertises which protocol versions, capabilities, and identity the server supports. Clients can call it ahead of other requests to pre-select a version, and on stdio it can serve as a backward-compatibility probe. This is the answer to what the roadmap called learning what a server does without connecting to it.

What gets removed: ping, logging/setLevel, and notifications/roots/list_changed disappear. Log level is now specified per request via io.modelcontextprotocol/logLevel in _meta, and for a request that lacks this field, servers MUST NOT emit notifications/message.

Subscription mechanism replaced: the HTTP GET endpoint and resources/subscribe / resources/unsubscribe are replaced by a single subscriptions/listen — one long-lived POST response stream for server-to-client change notifications. Clients opt in to the kinds they want (toolsListChanged, promptsListChanged, resourcesListChanged, resourceSubscriptions). One distinction worth noting — request-scoped notifications such as notifications/progress or notifications/message still flow through that request's own response stream, not through the subscriptions/listen stream.

And Here Is Where the Real Cost Sits

That same SEP-2575 also removes SSE stream resumability and message redelivery from Streamable HTTP — the Last-Event-ID header and SSE event IDs are gone. The changelog states it plainly: if the response stream breaks, the request in flight is lost, and the client MUST send a new request with a new request ID.

This is not a pure win but a trade. Resumability used to be the mechanism that let a long tool call survive a single network drop and keep receiving. With it gone, the reliability of long-running work now has to come from somewhere else — from the Tasks extension, covered later — rather than from the protocol core.

MRTR — How Servers Ask Follow-Up Questions Gets Flipped

Personally, I think this is the deepest change in this revision.

Until now, a server could ask the client something mid-processing — request a directory via roots/list, borrow an LLM via sampling/createMessage, or collect user input via elicitation/create. That assumes a live, bidirectional channel while the server holds the request open. Which is, fundamentally, stateful.

The MRTR (Multi Round-Trip Requests) pattern document replaces this. And it states it very plainly — servers MUST send server-to-client requests using the MRTR pattern, and the previous server-initiated request pattern is no longer supported; this is a breaking change.

The new flow looks like this:

1. Client -> Server :  tools/call (id: 1, params)
2. Server -> Client :  InputRequiredResult (id: 1)
                       resultType: "input_required"
                       inputRequests: { "github_login": ElicitRequest, ... }
                       requestState: "<opaque blob only the server can decode>"
   -> The original request ends here. The server holds nothing.

3. Client: asks the user and collects the input

4. Client -> Server :  tools/call (id: 2, original params
                                   + inputResponses + requestState)
   -> Must use a new JSON-RPC id — it is an independent request.

5. Server -> Client :  Result (id: 2, resultType: "complete")

The InputRequiredResult a server returns when it asks a follow-up question looks roughly like this:

{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "resultType": "input_required",
    "inputRequests": {
      "github_login": {
        "method": "elicitation/create",
        "params": {
          "mode": "form",
          "message": "Please provide your GitHub username",
          "requestedSchema": {
            "type": "object",
            "properties": { "name": { "type": "string" } },
            "required": ["name"]
          }
        }
      }
    },
    "requestState": "AEAD-protected blob"
  }
}

The sentence the document underlines is the point of this design — each step's request is fully independent, and a server handling a retry needs nothing beyond what is directly carried in that retry request. Which means no shared storage across instances, and no state-based load balancing, is needed.

A few detail rules:

  • A resultType field becomes mandatory on every result — ordinarily "complete", and "input_required" for an MRTR intermediate result. If a pre-existing-protocol server omits this field, clients MUST treat it as "complete".
  • Only three requests are allowed to receive an InputRequiredResult from a server: prompts/get, resources/read, and tools/call. Servers MUST NOT send one for anything else.
  • A server must not send a kind of inputRequests the client has not declared as a capability. If a client has not said it supports elicitation, the server cannot slip in elicitation/create.
  • Servers MUST NOT assume the client will retry. It may simply never come back.

The State Did Not Disappear — It Moved

Here is where honesty is required. MRTR does not eliminate server state; it stuffs it into an opaque blob called requestState, passes it through an untrusted client, and gets it back. And the spec does not hide the consequence.

If a client request carries a requestState, the server MUST treat it as attacker-controlled input. If this value affects authorization, resource access, or business logic, the server must protect its integrity (HMAC, AEAD, or similar) and must reject any state that fails validation. Skipping integrity protection is acceptable only when tampering cannot cause any harm beyond the request itself failing.

To resist replay, servers are recommended (SHOULD) to embed, inside the integrity-protected payload, an authenticated principal, a short TTL, and an identifier for the original request (such as a digest of the method name and key parameters), and to verify it every time. And to carry over the document's own warning as-is — these measures narrow the replay window and block reuse across users and requests, but they do not, by themselves, guarantee single-use. For cases that must be consumed exactly once — like a one-time redemption — the server itself must enforce that invariant (MUST).

To sum up, for server authors this is new homework. What used to be "just hold a session object in memory" has become "doing cryptography properly." The moment you put a user ID into requestState without a signature, that is an authorization bypass. Stateless is not free, and a large share of that cost lives right here.

One more thing — a retry is literally a re-execution of the original request. If the work the server did before it asked the follow-up question was expensive, that work either happens again on retry (cost), or has to ride along inside requestState (size and complexity).

What Actually Changes for Operators — Headers, Caching, Tracing

Less flashy than the stateless core, but this may be the part that people running gateways feel more.

Standard headers (SEP-2243). Mcp-Method and Mcp-Name headers become mandatory on Streamable HTTP POST requests. The goal is letting load balancers, gateways, and rate limiters route and throttle at the HTTP layer without tearing open the body. Support for x-mcp-header, which passes custom headers through tool parameters, ships alongside this.

Caching (SEP-2549). tools/list, prompts/list, resources/list, resources/read, and resources/templates/list results get ttlMs and cacheScope as a new, mandatory CacheableResult interface. ttlMs is a freshness hint in milliseconds that lets clients cache and poll less; cacheScope is "public" or "private" and controls whether a shared intermediary is allowed to cache it. It is modeled on HTTP's Cache-Control, and it does not replace the existing listChanged notifications — it complements them.

Worth noting that this caching field is a matched set with the fact that sessions are gone and lists no longer vary per connection. Stable lists are what make caching meaningful. In the same spirit, tools/list is recommended (SHOULD) to return tools in a deterministic order, and the reason is interesting — it is not just for client-side caching, but to raise the LLM prompt cache hit rate. This is the point where protocol design starts to be conscious of LLM inference cost itself.

Tracing (SEP-414). The convention for propagating OpenTelemetry trace context in _meta gets documented — the traceparent, tracestate, and baggage key names are pinned down, so distributed traces correlate across SDKs and gateways.

Error codes. The resource-not-found error moves from MCP's own -32002 to the JSON-RPC standard -32602 (Invalid Params). If you have client code matching the literal -32002, it needs fixing. Beyond that, an error-code allocation policy is now in place — -32000 through -32019 stay implementation-defined (existing SDK usage is grandfathered in), and -32020 through -32099 are reserved by the spec. Codes introduced earlier in this draft get renumbered accordingly — HeaderMismatch moves from -32001 to -32020, MissingRequiredClientCapability from -32003 to -32021, and UnsupportedProtocolVersion from -32004 to -32022.

Auth — Leaning Further Into OAuth/OIDC

Several SEPs have tightened auth incrementally.

  • SEP-2468: authorization servers are recommended (SHOULD) to include an iss parameter in the authorization response per RFC 9207, and if iss is present, MCP clients MUST validate it against the recorded issuer before redeeming the authorization code. This mitigates authorization-server mix-up attacks.
  • SEP-837: clients must specify an appropriate application_type during dynamic client registration (DCR), to avoid OpenID Connect redirect-URI collisions.
  • SEP-2352: client credentials are bound to the authorization server that issued them. Clients MUST key stored credentials by issuer identifier, MUST NOT reuse them against a different authorization server, and MUST re-register if the authorization server changes.
  • And DCR (RFC 7591) itself enters deprecation — Client ID Metadata Documents becomes the preferred registration mechanism. DCR remains usable for backward compatibility with authorization servers that do not support it yet.

What Is Going Away — Roots, Sampling, Logging

SEP-2577 marks three features — Roots, Sampling, and Logging — as deprecated. The proposed migration path is:

FeatureReplacement
Rootspass directories/files via tool parameters, resource URIs, or server config
Samplingintegrate directly with an LLM provider API
Loggingstderr on stdio; structured observability via OpenTelemetry

The important part is that nothing breaks right now. As the RC announcement puts it, the relevant methods, types, and capability flags keep working in this release, and in every spec version published within 1 year of this release. For now, this is only a comment attached.

And the reason this is even possible is a quiet but arguably more important change — SEP-2596 introduces a feature lifecycle and deprecation policy. A feature sits in one of three states — Active, Deprecated, or Removed — and a minimum 12-month window is guaranteed between deprecation and removal. The window is counted not from the day the SEP went Final, but from when the spec revision that marked the feature Deprecated was released. There is exactly one exception — the window can be shortened only for an active security risk backed by a published advisory or documented real-world exploitation with no alternative mitigation, and even then a minimum of 90 days must be given.

Attached to this is a registry that collects deprecated features in one place, so nobody has to reconstruct what disappears when by digging through changelogs scattered across revisions. Tier 1 SDKs carry an obligation to mark the corresponding API surface in each language's native way (@Deprecated, [Obsolete], @deprecated JSDoc, and so on).

Under this same policy, the HTTP+SSE transport (deprecated since 2025-03-26) and the "thisServer" / "allServers" values of includeContext are also formally reclassified as Deprecated.

Honestly, I think this governance change will outlast the stateless core in the ecosystem. The question of whether you can build a product on top of this protocol just got a date attached to its answer for the first time.

Extensions — Tasks Leaves the Core

SEP-2133 makes extensions a first-class concept. Each has a reverse-DNS identifier, is negotiated through the extensions map in capabilities, is versioned independently of the spec, and has its own repository and delegated maintainers.

Two official extensions land on top of this framework.

  • MCP Apps (SEP-1865): server-rendered HTML interfaces inside a sandboxed iframe. Tools declare UI templates, and actions from the UI take the same JSON-RPC path as a direct call.
  • Tasks (SEP-2663): for long-running work. This was originally an experimental core feature in 2025-11-25, and — to quote the RC announcement directly — production use surfaced enough of a need for redesign that an extension, rather than the spec, turned out to be its right home. The new design replaces the blocking tasks/result with tasks/get polling, adds tasks/update for client-to-server input, removes tasks/list, and lets servers return a task handle up front without requiring per-request opt-in.

This is where the earlier point about SSE resumability disappearing comes back around. The reliability of long-running work is now properly solved with task handles plus polling, not stream resumption. And one warning — anyone who already shipped against the experimental 2025-11-25 Tasks API needs to migrate to the new lifecycle.

Tool schemas loosen up too. SEP-2106 allows inputSchema and outputSchema to use the full JSON Schema 2020-12 keyword set (oneOf, anyOf, allOf, conditionals, $ref, $defs), and lets structuredContent accept arbitrary JSON values. Implementations, however, must not auto-dereference external $ref URIs, and must cap depth and validation time — more expressive schemas open a correspondingly larger attack surface.

What to Do Now — and What Not to Rush

Beta SDKs shipped on June 29, 2026 — all four Tier 1 SDKs.

Python      mcp==2.0.0b1                       (v2, needs an exact pin — otherwise you stay on 1.x)
TypeScript  @modelcontextprotocol/server@beta  (v2, server/client packages split
            @modelcontextprotocol/client@beta   ESM-only, Node.js 20+ / Bun / Deno)
Go          v1.7.0-pre.1                       (module path unchanged from stable)
C#          2.0.0-preview.1                    (dotnet add package ... --prerelease)

And to carry over the warning the announcement itself attaches — the stable SDK release is still the recommended version for important workloads, and public APIs can still change between beta and stable. Pin an exact version while testing.

On backward compatibility, it is unambiguous — existing servers keep working, and in the announcement's own words, nothing breaks today and nothing breaks on July 28 either. New clients that meet an older server fall back automatically to the legacy handshake.

Worth trying now

  • Grep your codebase for literal -32002 matches. This is one of the few spots that can break quietly.
  • If you built against the experimental 2025-11-25 Tasks API, plan the migration. This is confirmed homework.
  • If you run a remote server, work out how much sticky sessions and shared session storage actually cost you today. That is the ruler for what this revision is worth to you.
  • Poke at it with the beta SDK in a test environment. Not production.

Not worth rushing

This is the most important part. If you only run local servers over stdio, most of this revision is not about you. The stateless core is a remote, horizontal-scaling story from top to bottom. For a stdio server with one process tied to one client, sticky sessions were never a problem to begin with, and there is no round-robin load balancer either. For that kind of server, what comes back is not a benefit but pure migration cost — Roots, Sampling, and Logging eventually have to be removed, follow-up questions have to be rewritten for MRTR, and the payoff is not much.

The 12-month deprecation window exists precisely for people like this. If there is no reason to hurry, do not hurry.

Closing

To sum up: 2026-07-28 is the revision where MCP gives up state at the protocol layer. The handshake and Mcp-Session-Id disappear, every request describes itself through _meta, server/discover opens up discovery without connecting, and standard headers plus caching hints give gateways HTTP-like handles. The goal is singular — let any instance handle any request.

The cost is just as clear. How a server asks follow-up questions gets flipped by MRTR into a breaking change; state has not disappeared but now rides back and forth through an untrusted client inside a requestState blob, and the cryptography that makes that blob safe is now the server author's responsibility. The gap left by removed SSE resumability has to be filled by the Tasks extension. Roots, Sampling, and Logging are now on the clock.

And as of today, all of this is still just an RC. The current spec is 2025-11-25, and the final version arrives in 12 days. Once the spec is finalized, the document to re-read is not this blog post but the official changelog — this post's only job is to tell you what to look for when you go read that document.

References

현재 단락 (1/123)

Today is July 16, 2026. And MCP's **Current specification is still 2025-11-25** — that is what the [...

작성 글자: 0원문 글자: 19,497작성 단락: 0/123