MCP 2026-07-28: Stateless Core, Extensions, Tasks

What the 2026-07-28 revision of the Model Context Protocol changes and how the new mechanisms behave: seven stepped figures, their notes quoted or paraphrased from the specification and the extension specs, plus what to build with today

draft · updated 2026-08-227 studies20 min18 sources

In short

  1. Every request carries its own protocol version and capabilities; there is no handshake and no session, and a server that reuses anything from an earlier request is non-conformant
  2. A version or capability mismatch is an error you handle on the reply, not something you negotiate up front
  3. Declaring an extension does not commit the server to use it, and omitting one does not guarantee a plain result: the server may fall back or reject with -32021
  4. A task is durable before you see its id, the client drives polling, cancel is cooperative, and tasks/get must reach the instance holding the task
  5. Mid-call input is a retry, not a callback: input_required with inputRequests and opaque requestState, then the same request again with inputResponses, the echoed state and a new id
  6. An MCP App is a sandboxed iframe with its own ui/initialize handshake; the host pushes input and results, relays tool calls under its policy, and with no declared CSP the app reaches no network
  7. Enterprise-managed authorization centralizes token issuance, not traffic: SSO, ID-JAG token exchange at the IdP, JWT grant at the resource authorization server; revocation stops new grants, issued tokens live to expiry
Athe request model

Every request carries its own protocol version and capabilities; there is no handshake and no session, and a server that reads version, capabilities or identity from an earlier request is doing something the spec forbids

The 2026-07-28 revision declares MCP "a stateless protocol: all the information needed to process a request is contained in the request itself" (the request travels as JSON-RPC, a small id-and-method envelope). In practice, every request has to describe itself; the spec enforces that with two rules. Servers "MUST NOT rely on prior requests over the same connection to establish context (e.g., capabilities, protocol version, client identity)", and state that has to span requests "MUST be referenced by an explicit identifier the client passes on each request". Two fields in the request's _meta object (MCP's per-request metadata) are therefore required on every request, io.modelcontextprotocol/protocolVersion and io.modelcontextprotocol/clientCapabilities; a request missing either "is malformed" and gets -32602. On HTTP the version goes in an MCP-Protocol-Version header; an Mcp-Method header is required on all requests and an Mcp-Name header on tools/call, resources/read and prompts/get, "so that intermediaries (load balancers, gateways, observability tooling) can route and inspect requests without parsing the body".

There is no initialize: "There is no negotiation handshake. Every request carries its protocol version, and the server accepts or rejects each request independently". Sessions and Mcp-Session-Id are gone: a modern-only server receiving one should "ignore it, and do not mint or echo session IDs". The core spec does not promise that any instance can serve any request; SEP-2575 explains why the stateless design allows it in principle, and Study D shows where it breaks. To compare the two eras, use the toggle and step through each with Prev and Next; the legacy side is the 2025-11-25 specification, with the client sendingnotifications/initialized and the session being a MAY.

The 2026-07-28 call carries everything in step 3; the 2025-11-25 call depends on steps 1 and 2Sequence diagram with a switch between the legacy 2025-11-25 flow (initialize, InitializeResult with optional session id, initialized notification, tools/call with session id) and the modern 2026-07-28 flow (optional server/discover, tools/call carrying protocol version and capabilities in _meta and headers, result).The 2026-07-28 call carries everything in step 3; the 2025-11-25 call depends on steps 1 and 2same client and server lanes; legacy: initialize, optional session id, initialized, tool call; modern: optional discover, then a self-contained tool callClientServer1server/discover (optional)2DiscoverResult {supportedVersions, capabilities, _meta serverInfo}3tools/call + _meta {protocolVersion, clientCapabilities} + headers4result {resultType: "complete", _meta serverInfo}
1 / 4
1 · ClientServer · request server/discover (optional)

"Servers MUST implement server/discover. Clients MAY call it before sending any other requests to learn the server's supported versions up front, but are not required to."

Figure 1 · Read it: the modern flow has no step the server has to remember: discover is optional information, and the tool call carries version, capabilities and identity itself. The legacy flow's fourth step depends on what the server set up in its second. Flip point: the first time a server infers version, capabilities or identity from an earlier request, which the spec forbids with a MUST NOT. State referenced by an explicit identifier the client re-sends (a task id, a subscription id) is allowed. Held fixed: one client, one server, Streamable HTTP; stdio has no header layer and, against a possibly legacy server, recommends a server/discover probe first.

Put everything the server needs in the request. Treat server/discover as optional information, not a setup step.

  • Required on every request: protocolVersion and clientCapabilities in _meta; On HTTP the MCP-Protocol-Version header (its value "MUST" match the body or the server returns 400 with -32020), Mcp-Method on all requests, and Mcp-Name on tools/call, resources/read and prompts/get.
  • clientInfo and serverInfo are SHOULD and "are self-reported by the sender and are not verified by the protocol"; implementations "SHOULD NOT rely on them for security decisions".
  • On HTTP a broken response stream loses the in-flight request and "clients MUST re-issue it as a new request with a new request ID"; on stdio a restart loses in-flight requests and "the client can retry them against the fresh process". The re-issue needs no handshake.
Simplified: the figure shows Streamable HTTP. On stdio "All messages share this single channel"; the same statelessness rules apply, and "an open connection, such as a STDIO process, is not a conversation or session".
Go deeper

Dual-era servers exist: "A request carrying modern per-request _meta is served statelessly according to this revision. An initialize request selects legacy semantics", and a modern-only server "SHOULD name the protocol versions it supports in any error it returns to an initialize request". The specification's own index page still says extensions are "negotiated during initialization"; the normative pages say per request.

Bversions and capabilities

A version or capability mismatch is an error you handle on the reply, not something you negotiate up front; a probe is recommended only on stdio when the server may be legacy

With no handshake, compatibility is checked per request. If the server does not implement the version a request names, it "MUST respond with an UnsupportedProtocolVersionError listing the versions it does support" (-32022, HTTP 400), and "The client SHOULD select a mutually supported version from the supported list and retry the request". If the request needs a client capability the client did not declare, the server "MUST return a MissingRequiredClientCapabilityError (-32021) whose data.requiredCapabilities lists the missing capabilities". Both are ordinary replies; the retry carries the fix.

server/discover exists for clients that want to know first: "a client is free to invoke any RPC inline and handle UnsupportedProtocolVersionError if its preferred version is not supported". The exception is stdio with a possibly legacy server, where "some legacy servers do not validate that a request arrives after initialize and would process an era-ambiguous method (such as tools/call) under legacy semantics", so on stdio a dual-era client "SHOULD probe with server/discover before sending any other request", and even a modern-only client is RECOMMENDED to probe. Switch between the two errors and step through.

Each error carries what the retry needs; no negotiation state is kept on either sideSequence diagram with a switch between an unsupported protocol version (error -32022 listing supported versions, then a retry with one of them) and a missing client capability (error -32021 listing required capabilities, then a retry declaring it).Each error carries what the retry needs; no negotiation state is kept on either sidea version the server lacks, then a capability the client did not declare, each followed by one retryClientServer1tools/call (_meta protocolVersion: "1900-01-01")2400 + UnsupportedProtocolVersionError (-32022) {supported: [...]}3tools/call (_meta protocolVersion from "supported")4result
1 / 4
1 · ClientServer · request tools/call (_meta protocolVersion: "1900-01-01")

The client sends its preferred version inline; no handshake has checked it.

Figure 2 · Read it: both errors carry the information the retry needs (supported, requiredCapabilities), and the retry is a fresh, complete request. Flip point: on stdio against a possibly legacy server, send server/discover first (SHOULD for dual-era clients, RECOMMENDED even for modern-only); everywhere else, sending the real request is allowed. Held fixed: the error shapes of the 2026-07-28 base protocol; extension-specific errors are in Study C.

Handle -32022 and -32021 as retry instructions. Probe with server/discover on stdio when the server could be legacy, or anywhere you want the information (versions, extensions, instructions) before the first call.

  • -32022: pick from data.supported and retry, "or surface an error to the user if no compatible version exists".
  • -32021: add the capabilities in data.requiredCapabilities if you can honour them; "Servers MUST NOT infer capabilities from prior requests" (the schema doc on the per-request capability field), so declare them on the retry itself.
  • -32602 is different: the request was malformed (a required _meta field missing), not incompatible.
Go deeper

Reserved error codes added by this revision: -32020 HeaderMismatch, -32021 MissingRequiredClientCapability, -32022 UnsupportedProtocolVersion; -32002 and -32042 "MUST NOT" be emitted by this version. Discover responses carry ttlMs and cacheScope: "Caches MUST NOT be shared across authorization contexts".

Cextensions

Declaring an extension does not commit the server to use it. Omitting one does not guarantee a plain result either: the server may fall back to core behaviour, or reject the request with -32021

Extensions are "identified using a unique extension identifier with the format: {vendor-prefix}/{extension-name}"; four are official today: OAuth Client Credentials, Enterprise-Managed Authorization, MCP Apps (io.modelcontextprotocol/ui) and MCP Tasks (io.modelcontextprotocol/tasks). They are declared per request: "Clients advertise extension support in _meta["io.modelcontextprotocol/clientCapabilities"] within each request" and "Servers advertise extension support in the server/discover response". "Extensions are always disabled by default and require explicit opt-in from the developer."

When one side lacks an extension, the normative rule is: "If one party supports an extension but the other does not, the supporting party MUST either revert to core protocol behavior or reject the request with an appropriate error". For Tasks both outcomes are spelled out. A server "MUST NOT return CreateTaskResult to a client that did not include the extension capability on its request". A server that cannot serve the request without a task "MUST return an error with the code -32021". Declaring does not force the server's hand either: it "MAY return CreateTaskResult ... at its own discretion and on a per-request basis. The server is the sole decider". Switch between a client that declares Tasks and one that omits it.

Declared or omitted, the server decides what comes backSequence diagram with a switch between a client that declares the tasks extension (server may return CreateTaskResult or CallToolResult; client must handle both) and a client that omits it (server returns a plain CallToolResult or rejects with -32021 if it cannot serve synchronously).Declared or omitted, the server decides what comes backthe tasks extension as the example; declared: task handle or plain result; omitted: plain result or -32021ClientServer1tools/call + clientCapabilities.extensions {io.modelcontextprotocol/tasks: {}}2CreateTaskResult {resultType: "task"} or CallToolResult3handle either shape
1 / 3
1 · ClientServer · request tools/call + clientCapabilities.extensions {io.modelcontextprotocol/tasks: {}}

"Clients advertise extension support in _meta["io.modelcontextprotocol/clientCapabilities"] within each request". An empty settings object "indicates support with no additional settings".

Figure 3 · Read it: with the extension declared, the second step has two legal shapes and the client must be ready for both. With it omitted, the second step has two legal outcomes too: a core result, or a rejection. Flip point: a server that cannot serve the call synchronously returns -32021 to a client that omitted Tasks. Held fixed: the Tasks extension; the informative guidance for Apps is the softer one ("a server offering UI-enhanced tools should still return meaningful text content for clients that don't support the UI extension"), for auth the harder one ("can reject connections from clients that don't support it").

Declare the extensions you can honour on every request, handle every shape the extension allows back, and treat -32021 as the server telling you which extension it needed.

  • Client: "MUST be prepared to handle either CallToolResult or CreateTaskResult in response to any supported request it issues" once Tasks is declared.
  • Server: decide per request; never return an extension result to a request that did not declare the extension, "regardless of prior declarations".
  • Changing an extension: "prefer using capability flags or versioning within the extension settings object rather than creating a new extension identifier".
Go deeper

Governance: experimental extensions live in repositories with the experimental-ext- prefix and graduate "through the standard SEP process (Extensions Track)", which requires "at least one reference implementation in an official SDK". One inconsistency in the docs: the client matrix page lists three identifiers and omits Tasks; the overview lists four.

Dtasks

A task is durable before you see its id, the client drives polling, cancel is a request the server may ignore, and tasks/get is routed by taskId to the instance that holds the task

The Tasks extension lets a server "respond to a tools/call request with an asynchronous task handle instead of a final result, allowing the client to retrieve the eventual result by polling"; in this version "The following methods currently support task-augmented execution: tools/call". Durability is a MUST at creation: "A server MUST NOT return CreateTaskResult until the task is durably created - that is, until a tasks/get for the returned taskId would resolve". Retention is not: clients "MAY treat the TTL as a backstop", servers "MAY mark a task as failed at any point after the TTL elapses, and subsequently delete it", and "It is compliant behavior for a server to return an error stating the task cannot be found if it has purged an expired task".

Two facts cut against the "any instance" reading of the core. Polling requests carry a routing key: "the client MUST set the Mcp-Name header to the value of params.taskId. This allows transport intermediaries and load balancers to route subsequent requests for the same task to the server instance holding its state, which is typically required for correctness". And cancellation is "cooperative: The request signals intent, and the server decides whether and when to honor it ... Eventual transition to cancelled is not guaranteed". Step through the happy path, then the cancel path.

Every poll after the handle carries the taskId, in params and in the Mcp-Name headerSequence diagram with a switch between a full task lifecycle (task handle, polling with the taskId routing header, an input_required state answered with tasks/update, completion) and a cancellation (tasks/cancel acknowledged, then a poll that may show cancelled, working or completed).Every poll after the handle carries the taskId, in params and in the Mcp-Name headerCreateTaskResult, tasks/get with Mcp-Name = taskId, input_required via tasks/update, terminal statesClientServer1tools/call render_report (tasks extension declared)2CreateTaskResult {resultType: "task", taskId, status, pollIntervalMs, ttlMs}3tasks/get {taskId} (Mcp-Name: taskId)4GetTaskResult {status: "working"}5tasks/get {taskId}6GetTaskResult {status: "input_required", inputRequests}7tasks/update {taskId, inputResponses}8UpdateTaskResult (empty acknowledgement)9tasks/get {taskId}10GetTaskResult {status: "completed", result}
1 / 10
1 · ClientServer · request tools/call render_report (tasks extension declared)

Only tools/call supports task-augmented execution in this version of the extension.

Figure 4 · Read it: every client step after the first carries the taskId, twice: in params and in the Mcp-Name header the load balancer reads. Flip point: that header is the moment the stateless core admits a stateful endpoint, since the load balancer must send the poll to the exact instance holding the task. Note the final state: "completed ... includes tool calls that returned results with isError: true"; only a JSON-RPC error during execution is failed. Held fixed: Streamable HTTP; polling; the optional subscriptions/listen push of notifications/tasks is not drawn.

Use Tasks for work longer than a request; route by taskId; persist task ids on the client; check the result inside "completed"; and do not trust cancel to stop anything.

  • Clients "SHOULD respect the pollIntervalMs" and "SHOULD persist task IDs to durable storage so that polling can resume after a crash or restart".
  • Input during a task goes through tasks/get and tasks/update, "not via retries of the original method"; input needed before the handle exists uses multi round-trip requests (MRTR, Study E) on the original request.
  • Task ids may act "as bearer tokens for a server's stored state"; servers "MUST generate them with sufficient entropy" and "MUST perform authentication and authorization checks on each task-related request".
Simplified: the figure shows one task and client-driven polling. Servers "MAY push status updates via notifications/tasks" on a subscriptions/listen stream; progress and message notifications "are not supported on tasks in general in this specification".
Go deeper

Shapes: Task has taskId, status (working, input_required, completed, cancelled, failed), createdAt, lastUpdatedAt, ttlMs (null for unlimited), pollIntervalMs; CreateTaskResult sets resultType: "task", and a tasks/get result carries resultType: "complete" because it is the ordinary result of that request. The ext-tasks overview page still shows -32003 for the missing-capability error; the normative file and the core spec use -32021.

Emid-call input

Mid-call input is a retry, not a callback. The server answers with input_required plus an opaque requestState; the client re-sends the original request with the answers, the same state and a new id

The server can no longer open its own request to the client mid-call (there is no held-open stream to carry it): "Servers MUST send server-to-client requests (such as roots/list, sampling/createMessage, or elicitation/create) using the MRTR pattern. The previous pattern of server-initiated requests is no longer supported. This is a breaking change." The pattern is four steps: the client sends a request; the server "responds requesting more information"; the client gathers it and "retries the original request including the additional requested information"; the server answers with the final result. Multi round-trip requests (MRTR) exist so that servers can ask "without requiring a shared storage layer across server instances or requiring stateful load balancing".

The rules that make it safe are on both sides. requestState "is an opaque string meaningful only to the server", and the client "MUST echo back the exact value ... MUST NOT inspect, parse, modify, or make any assumptions about" it. The server, in turn, "MUST treat requestState as an attacker-controlled input. If requestState influences authorization, resource access, or business logic, servers MUST protect its integrity (e.g. HMAC or AEAD)". The retry is a new request: "The JSON-RPC id MUST be different between the initial request and the retry, as they are independent requests". Step through one round trip.

The retry is the first request again, plus inputResponses and the echoed requestState, under a new idSequence diagram of a multi round-trip request: the client calls a tool, the server returns input_required with an elicitation request and opaque state, the client gathers the input and retries the same tool call with the responses and the echoed state under a new id, and the server completes.The retry is the first request again, plus inputResponses and the echoed requestState, under a new idtools/call, InputRequiredResult with inputRequests and requestState, the same call again with inputResponses, new idClientServer1tools/call render_report (id 1)2InputRequiredResult {inputRequests {q1: elicitation/create}, requestState}3gather the requested input (user, model, or roots)4tools/call render_report (id 2, inputResponses {q1: ElicitResult}, requestState echoed)5result {resultType: "complete"}
1 / 5
1 · ClientServer · request tools/call render_report (id 1)

Servers "MAY send InputRequiredResult responses on the following client requests: prompts/get, resources/read, tools/call" and "MUST NOT" on any other.

Figure 5 · Read it: the third step is the only one that is not a message: the client does work (asks the user, calls a model, lists roots) and then sends what looks like the first request plus two fields. Flip point: the moment requestState "influences authorization, resource access, or business logic", integrity protection stops being advice and becomes MUST. Held fixed: one input request; the server may also return a new InputRequiredResult if the retry is incomplete, "rather than returning an error".

Put what you need to resume into requestState, sign it (HMAC or AEAD) if it matters, and never assume the client will come back.

  • Allowed on prompts/get, resources/read and tools/call only (the spec lists the three in a table); the requests inside "MUST be one of ElicitRequest, CreateMessageRequest, or ListRootsRequest", and only ones the client "has declared support for in its capabilities".
  • Replay: servers "SHOULD include ... the authenticated principal ... a short expiry (TTL) ... an identifier for the originating request" inside the protected state, and these "do not by themselves guarantee single-use".
  • "Servers MUST NOT assume that clients will fulfill the inputRequests or retry the original request." Interim input_required results "are not cacheable", and retries "MUST NOT be cached".
Go deeper

Shapes: InputRequiredResult is a Result with resultType: "input_required", optional inputRequests (a map of server-assigned keys to request objects with method and params) and optional requestState; servers "MUST include at least one of inputRequests or requestState in every InputRequiredResult response". The retry params carry inputResponses (same keys, result objects such as ElicitResult) and requestState. resultType values are complete, input_required, and extension values such as Tasks' task; an absent resultType is read as complete.

Fmcp apps

An MCP App is a sandboxed iframe with its own ui/initialize handshake; the host pushes tool input and results to it, relays its tool calls under the host's policy, and with no declared content security policy (CSP) it can make no fetch, XHR or WebSocket call

A tool points at a UI resource through _meta.ui.resourceUri; the resource's URI "MUST start with ui:// scheme", its mime type "MUST be text/html;profile=mcp-app", and the content "MUST be valid HTML5 document". The host fetches it with resources/read ("Host MAY prefetch and cache UI resource content") and renders it where "All View content MUST be rendered in sandboxed iframes with restricted permissions"; a web host "MUST wrap the View and communicate with it through an intermediate Sandbox proxy" on a different origin, over the browser postMessage channel.

Inside the sandbox the app is a client of its host: "Conceptually, UI iframes act as MCP clients, connecting to the host via a postMessage transport", and the dialect keeps a handshake the core protocol dropped, ui/initialize and ui/notifications/initialized; "The Host MUST NOT send any request or notification to the View before it receives an initialized notification". Once initialized, the host pushes tool data: it "MUST send" ui/notifications/tool-input and "MUST send" ui/notifications/tool-result "when tool execution completes (if the View is displayed during tool execution)". The app's own tools/call messages go back through the host, which "MAY forward" them or "MAY decide to block some messages or subject them to further user approval". Step through a render and one round trip.

Fetch, sandbox, handshake, push, relay: the host sits between the view and the serverSequence diagram of an MCP App: the host calls a tool with a UI resource, reads the ui:// HTML, renders it in a sandboxed iframe, the view initializes with the host, the host pushes tool input and result, the view calls a tool through the host, and the host relays the server's result.Fetch, sandbox, handshake, push, relay: the host sits between the view and the serverhost, server and view lanes; ui:// resource, ui/initialize, tool-input and tool-result notifications, host-mediated tools/callHostServerView1tools/call (tool with _meta.ui.resourceUri)2resources/read ui://…3HTML (text/html;profile=mcp-app) + _meta.ui {csp, permissions}4render in a sandboxed iframe (web hosts: separate-origin sandbox proxy)5ui/initialize {appCapabilities}6McpUiInitializeResult {host capabilities, sandbox}7ui/notifications/initialized8ui/notifications/tool-input, then ui/notifications/tool-result9tools/call (JSON-RPC over postMessage)10tools/call (forwarded, host policy)11result, relayed to the View
1 / 11
1 · HostServer · request tools/call (tool with _meta.ui.resourceUri)

The tool declares "URI of UI resource for rendering tool results" under Tool._meta.ui. "Tools MUST return meaningful content array even when UI is available."

Figure 6 · Read it: the view never talks to the server; every message crosses the host lane, in both directions. Flip point: a resource with no ui.csp gets the default policy, which ends in connect-src 'none': no fetch, XHR or WebSocket from the app (scripts, images and media from the sandbox origin and data: still load); declared domains open exactly those, and "Host MAY further restrict but MUST NOT allow undeclared domains". Held fixed: one tool, one view; ui/notifications/tool-input-partial streaming, display-mode and open-link requests are not drawn.

Declare the domains the app needs in _meta.ui.csp, keep the tool useful without the UI, and design as if the host will gate every call.

  • Default CSP when ui.csp is omitted: "default-src 'none'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; media-src 'self' data:; connect-src 'none';".
  • Visibility: "Host MUST NOT include tools in the agent's tool list when their visibility does not include "model""; "Host MUST reject tools/call requests from apps for tools that don't include "app" in visibility".
  • Fallback: "Servers SHOULD provide text-only fallback behavior for all UI-enabled tools"; "Tools MUST return meaningful content array even when UI is available".
Simplified: the ext-apps specification (stable 2026-01-26) still shows capability negotiation through an initialize request with a 2024 protocol version; under the 2026-07-28 core the io.modelcontextprotocol/ui capability (with its required mimeTypes setting) travels in per-request clientCapabilities.extensions.
Genterprise auth

Enterprise-managed authorization moves the access decision to the company identity provider (IdP). The client signs in, trades the sign-in for an ID-JAG (an identity-assertion grant) at the IdP, then trades that for an access token at the server's authorization server. The IdP sees only that issuance, not the traffic after

Four actors: the MCP client, the MCP server (the resource server), the Resource Authorization Server "that issues access tokens for the MCP Server", and the enterprise IdP "used for single sign-on". Three normative steps: "1. Single Sign-On to the MCP Client via OpenID Connect or SAML ... 2. Token Exchange (RFC8693) 3. JWT Authorization Grant (RFC7523)". The policy decision sits in step 2: "The IdP evaluates administrator-defined policies for the token exchange request and determines if the MCP Client should be granted access to act on behalf of the user for the target MCP Server and scopes"; if not, the client gets an OAuth token error and never holds an ID-JAG.

What the profile guarantees about the token: the ID-JAG "is a JWT issued and signed by the IdP" and the access token the resource authorization server issues "MUST be audience-restricted to the MCP Server identified by the resource claim in the ID-JAG". What it does not guarantee is control of traffic: "The visibility the IdP has between the MCP Client and MCP Server is limited to the process of issuing the access token, but does not extend to the actual MCP traffic". The informative overview's "taking effect immediately across all MCP clients" describes issuance; the normative profile says only that the IdP's visibility "does not extend to the actual MCP traffic", and the example access token carries expires_in of 86400 seconds. Step through the three exchanges.

The IdP decides at the token exchange and sees nothing of the traffic afterSequence diagram of enterprise-managed authorization: single sign-on yields an identity assertion, a token exchange at the IdP yields an ID-JAG or an error, a JWT bearer grant at the resource authorization server yields an audience-restricted access token, and the client calls the MCP server with it.The IdP decides at the token exchange and sees nothing of the traffic afterclient, IdP, resource authorization server and MCP server lanes; RFC 8693 at the IdP, RFC 7523 at the resource authorization serverClientIdPResource ASMCP Server1single sign-on (OpenID Connect or SAML)2ID Token (identity assertion)3token exchange (RFC 8693): ID token in, audience = Resource AS, requested id-jag4ID-JAG (JWT, typ oauth-id-jag+jwt) or an OAuth token error5token request: grant_type = jwt-bearer (RFC 7523), assertion = ID-JAG6access token (audience-restricted to the MCP server)7MCP request with the access token8result
1 / 8
1 · ClientIdP · request single sign-on (OpenID Connect or SAML)

Step 1: "A user logs in to an MCP Client through their enterprise Identity Provider, resulting in an Identity Assertion (ID Token or SAML assertion) being issued to the MCP Client."

Figure 7 · Read it: the IdP appears in two exchanges and then leaves the picture; the last two steps are ordinary bearer-token calls it does not see. Flip point: revoke at the IdP and the next token exchange fails; the profile does not say an already-issued access token is revoked, and its example token carries expires_in of 86400 seconds. Held fixed: the SAML variant (1a, refresh token) and discovery via authorization_grant_profiles_supported are not drawn.

Use this profile to remove per-server consent and put admission policy at the IdP; size access-token lifetimes to how fast revocation must bite, because that is where revocation stops.

  • Token exchange request: grant_type=urn:ietf:params:oauth:grant-type:token-exchange, requested_token_type=urn:ietf:params:oauth:token-type:id-jag, "audience MUST be the issuer identifier of the Resource Authorization Server".
  • Access token request: "grant_type urn:ietf:params:oauth:grant-type:jwt-bearer and the ID-JAG as the assertion".
  • Discovery: look for urn:ietf:params:oauth:grant-profile:id-jag in the authorization server's authorization_grant_profiles_supported.
Simplified: vendor names (Okta, Azure AD) appear in the informative overview only as examples; the normative profile names none. Client support today is uneven (the SDK table under Decisions): C# and TypeScript implement the client side, Python and Go parts of it.

Decision table

StudySituationCallWhat would change it
ADesigning a 2026-07-28 client or serverPut everything the server needs in the request; treat server/discover as optional informationDual-era servers select legacy semantics on an initialize request; stdio dual-era clients probe first
BHandling -32022 and -32021Retry with a supported version or the missing capability; probe with server/discover only on dual-era stdio or when you want the information firstA -32602 is a malformed request, not a mismatch
CDeclaring and handling extensionsDeclare what you can honour on every request; handle every shape the extension allows back; read -32021 as the extension the server neededApps guidance favours text fallback; auth extensions may reject
DLong-running tool callsUse Tasks for work longer than a request; route by taskId (Mcp-Name); persist task ids; check inside completed; do not trust cancelPush via subscriptions/listen if the server offers it
EAsking the user or the model mid-callCarry resume state in requestState, sign it if it affects authorization, never assume the client returnsInput during a running task goes through tasks/update instead
FShipping an interactive UI from a serverDeclare csp domains, keep the tool useful without the UI, design for a gating hostThe ext-apps spec still negotiates via initialize; under the 2026 core the capability travels per request
GEnterprise access to MCP serversUse the profile to move admission policy to the IdP and remove per-server consent; size access-token lifetimes to the revocation latency you can acceptClient support is uneven across SDKs today

Decisions

Two tables an engineer needs before building on this revision: what to build with today, and what to stop using. Every cell carries its source; the SDK table is a scan dated 2026-08-22.

1 · Build on 2026-07-28 today?

Call: Build the whole revision on C# v2.2.0 or, minus Apps and enterprise auth, on Rust rmcp 3.1.4. On Python 2.0.0, Go 1.7.0 and TypeScript 2.0.0 you get the stateless core and MRTR but not Tasks: Python's Tasks runtime is open PR #3005, Go has none (PR #755 covers capability negotiation only), and TypeScript removed the 2025-era tasks and tracks a rebuild in issue #2189. TypeScript's MCP Apps still lives in an ext-apps package pinned to the 1.x SDK. Stay on 2025-11-25 in Java, Kotlin and Swift.

Evidence table
SDKRelease (date)2026-07-28StatelessTasksMRTRAppsEnterprise authNote
C#v2.2.0 (2026-08-13)yesyesyesyesyesyesStateless is the default session mode; Tasks and Apps as extension packages; client-side ID-JAG (RFC 8693 + 7523). release
Rust (rmcp)rmcp-v3.1.4 (2026-08-20)yesyesyesyesnonoProtocolVersion::LATEST is still 2025-11-25; Apps is a capability field only; ROADMAP lists tasks conformance scenarios as expected failures. release
Pythonv2.0.0 (2026-07-28)yesyesopen-pryesyespartialTasks runtime is PR #3005 (open); enterprise auth implements the jwt-bearer leg only. release
TypeScript2.0.0 (2026-07-27)yesyesnoyesnoyes2025-era tasks removed (PR #2128), extension tracked in issue #2189; Apps live in ext-apps v1.7.5, which pins the 1.x SDK. release
Gov1.7.0 (2026-07-28)yesyesnoyesnopartial2026-07-28 only with StreamableHTTPOptions.Stateless = true; Tasks PR #755 open; RFC 8693 exchange only. release
Javav2.0.1 (2026-08-19)nonononononoCaps at 2025-11-25; ROADMAP plans 3.x for 2026-07-28 with first milestones in September 2026. release
Kotlin0.15.0 (2026-07-28)nonotypes-onlynononoCaps at 2025-11-25; 2025-era task wire types only. release
Swift0.12.1 (2026-05-07)nonononononoCaps at 2025-11-25. release

Scan method and per-cell evidence (tag-pinned files, PR states) are in the record. Cell values: yes (runtime shipped), partial, types-only, open-pr, no (not found at the tag). PHP and Ruby were not rescanned.

2 · What to stop using

Call: Stop adopting six features now. The four deprecated in this revision (roots, sampling, logging, dynamic client registration) get the policy's minimum twelve-month window; the HTTP+SSE transport is eligible for removal three months after SEP-2596 reaches Final, and the includeContext values follow sampling. A longer list was removed outright in 2026-07-28 and is gone: the initialize handshake, sessions, the HTTP GET stream and SSE resumability, server-initiated requests, resources/subscribe, ping, logging/setLevel and the in-core tasks methods. Migration paths below are the spec's own words.

Evidence table
FeatureStatusUse instead
Rootsdeprecated"Pass directories or files via tool parameters, resource URIs, or server configuration"
Samplingdeprecated"Integrate directly with LLM provider APIs"
Loggingdeprecated"Log to stderr for stdio transports; use OpenTelemetry for observability"
Dynamic Client RegistrationdeprecatedClient ID Metadata Documents
HTTP+SSE transportdeprecatedStreamable HTTP
includeContext valuesdeprecated"Omit the field or use \"none\""
initialize / notifications/initializedremoved in 2026-07-28per-request _meta; server/discover
Mcp-Session-Id and protocol-level sessionsremoved in 2026-07-28explicit identifiers carried on each request (taskId, subscriptionId)
HTTP GET endpoint, SSE resumability (Last-Event-ID)removed in 2026-07-28each message is a new POST; a broken stream loses the in-flight request, "clients MUST re-issue it"
Server-initiated JSON-RPC requestsremoved in 2026-07-28MRTR: requests embedded in InputRequiredResult
resources/subscribe, ping, logging/setLevel, in-core tasksremoved in 2026-07-28subscriptions/listen; none; _meta logLevel; the Tasks extension

Deprecated rows: the registry in the specification and SEP-2577 for roots, sampling and logging. Removed rows: the revision changelog. SEP-2577 lists "low adoption" as one of three reasons for deprecating sampling (with implementation complexity and direct alternatives); the spec says nothing about inference cost.

References

  1. Model Context Protocol (2026). MCP specification 2026-07-28, Base Protocol: statelessness, per-request _meta fields, resultType (docs/specification/2026-07-28/basic/index.mdx at 5b38eb17) · docs
  2. Model Context Protocol (2026). MCP specification 2026-07-28, schema (schema.ts / schema.mdx): RequestMetaObject, resultType, ResultType union; per-request capability declaration · docs
  3. Model Context Protocol (2026). MCP specification 2026-07-28, Streamable HTTP transport: required headers, error statuses, legacy mechanisms removed (basic/transports/streamable-http.mdx) · docs
  4. Model Context Protocol (2026). MCP specification 2026-07-28, Versioning: no negotiation handshake, UnsupportedProtocolVersionError, extensions in capabilities, fallback or reject (basic/versioning.mdx) · docs
  5. Model Context Protocol (2026). SEP-2575: Stateless MCP (rationale: 'any request can be handled by any server instance'; seps/2575-stateless-mcp.md) · docs
  6. Model Context Protocol (2025). MCP specification 2025-11-25, Lifecycle and Transports: initialize, initialized notification, optional MCP-Session-Id · docs
  7. Model Context Protocol (2026). MCP specification 2026-07-28, stdio transport: single channel, restart loses in-flight requests, server/discover probe for dual-era clients (basic/transports/stdio.mdx) · docs
  8. Model Context Protocol (2026). MCP specification 2026-07-28, Changelog: major and minor changes with SEP numbers, removals, error codes, deprecation window (changelog.mdx) · docs
  9. Model Context Protocol (2026). MCP Extensions overview: identifiers, per-request negotiation, official roster of four, fallback guidance, governance (docs/extensions/overview.mdx) · docs
  10. Model Context Protocol (2026). MCP specification 2026-07-28, Caching utility: ttlMs, cacheScope, authorization contexts, MRTR results not cacheable (server/utilities/caching.mdx) · docs
  11. Model Context Protocol (2026). MCP Tasks extension specification (ext-tasks specification/draft/tasks.md at e4345978; SEP-2663 Final): shapes, lifecycle, durability, polling, routing by taskId, cancellation, security · docs
  12. Model Context Protocol (2026). MCP specification 2026-07-28, Multi Round-Trip Requests (basic/patterns/mrtr.mdx): InputRequiredResult, requestState rules, retry rules · docs
  13. Model Context Protocol (2026). MCP Apps extension specification, stable 2026-01-26 (ext-apps specification/2026-01-26/apps.mdx at 10195ad9): ui:// resources, sandboxing and CSP, ui/initialize, host notifications, visibility · docs
  14. Model Context Protocol (2026). Enterprise-Managed Authorization profile, stable (ext-auth specification/stable/enterprise-managed-authorization.mdx at fb374c7d): actors, three steps, token exchange and JWT grant requirements, section 7.2 visibility · docs
  15. Model Context Protocol (2026). Enterprise-Managed Authorization overview (informative; docs/extensions/auth/enterprise-managed-authorization.mdx): centralized revocation wording, IdP examples · docs
  16. Victor Dibia (2026). MCP SDK status for the 2026-07-28 revision, scanned 2026-08-22 (latest tags, protocol constants, feature markers, PR states; per-cell evidence) · docs
  17. Model Context Protocol (2026). MCP specification 2026-07-28, Deprecated features registry: lifecycle policy and migration paths (deprecated.mdx) · docs
  18. Model Context Protocol (2026). SEP-2577: Deprecate roots, sampling and logging (seps/2577-deprecate-roots-sampling-and-logging.md) · docs
Revisions and earlier versions

Earlier versions are kept in the repository history. Revisions are cut after substantial changes only.

r1 (draft, unreleased) · 2026-08-22
  • all · added First release (in preparation): rebuilt on the study-page kit after a spec deep dive (modelcontextprotocol at 5b38eb17, ext-tasks, ext-apps, ext-auth) and an SDK scan; the previous page's flows were audited step by step and corrected (legacy flow direction and optional session, Tasks routing by taskId, MRTR requestState and id rules, the Apps ui/initialize handshake, the three auth steps, the extension fallback-or-reject rule).