Implement MCP 2026-07-28 (#2286)

Co-authored-by: Matt <77928207+mattzcarey@users.noreply.github.com>
Co-authored-by: Konstantin Konstantinov <KKonstantinov@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
This commit is contained in:
Felix Weinberger 2026-06-26 14:01:10 +01:00 • committed by GitHub
commit 1823aae891
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 69131 additions and 9001 deletions

44
docs/migration/index.md Normal file
View file

@ -0,0 +1,44 @@
# MCP TypeScript SDK — Migration Guides
Pick the guide for your starting point.
## Upgrading from v1.x (`@modelcontextprotocol/sdk`)
→ **[upgrade-to-v2.md](./upgrade-to-v2.md)**
You are on the monolithic `@modelcontextprotocol/sdk` package and want to move to the
v2 packages (`@modelcontextprotocol/client`, `@modelcontextprotocol/server`, …).
Start by running the codemod:
```bash
npx @modelcontextprotocol/codemod@alpha v1-to-v2 ./src
```
The codemod handles most mechanical renames. The guide covers what it can't. The
codemod handles the v1→v2 SDK surface upgrade only — adopting the 2026-07-28 protocol
revision (`createMcpHandler`, multi-round-trip requests, `versionNegotiation`) is
architectural and not codemod-automatable; see [support-2026-07-28.md](./support-2026-07-28.md).
## Already on v2, adopting protocol revision 2026-07-28
→ **[support-2026-07-28.md](./support-2026-07-28.md)**
You are already on the v2 packages and want your server or client to speak the
2026-07-28 protocol revision (per-request `_meta` envelope, `createMcpHandler`,
`serveStdio`, `versionNegotiation`, multi-round-trip requests, per-era wire codecs).
This guide also covers code written against an earlier **v2 alpha** that read
wire-only members (`resultType`, envelope keys) directly.
## Using an LLM agent to migrate
[upgrade-to-v2.md](./upgrade-to-v2.md) is the agent skill — it carries skill
frontmatter and is structured for mechanical application. Point the agent at
the codemod first; the guide is the codemod's companion for what's left.
## See also
- [`@modelcontextprotocol/codemod` README](../../packages/codemod/README.md)
- [FAQ](../faq.md)
- [Examples](https://github.com/modelcontextprotocol/typescript-sdk/tree/main/examples)

View file

@ -0,0 +1,443 @@
# Supporting protocol revision 2026-07-28
This guide is for code **already on the v2 packages** that wants to speak the 2026-07-28
protocol revision — and for code written against an earlier **v2 alpha** that read
wire-only members directly. If you are on `@modelcontextprotocol/sdk` (v1.x), start with
[upgrade-to-v2.md](./upgrade-to-v2.md) instead.
Nothing in v2 puts a 2026-07-28 byte on the wire by default: a hand-constructed
`Client` / `Server` / `McpServer` keeps speaking the 2025-era protocol it was written
for. Serving or speaking 2026-07-28 is always an explicit opt-in via one of the entries
below.
## Contents
- [Serving the 2026-07-28 revision](#serving-the-2026-07-28-revision)
- [Replacing per-session state: `requestState`](#replacing-per-session-state-requeststate)
- [Auth on 2026-07-28](#auth-on-2026-07-28)
- [Per-era wire codecs](#per-era-wire-codecs)
- [Wire-only members hidden from public types](#wire-only-members-hidden-from-public-types)
- [Multi-round-trip requests](#multi-round-trip-requests)
- [`subscriptions/listen`](#subscriptionslisten)
- [`Mcp-Param-*` and standard headers (SEP-2243)](#mcp-param--and-standard-headers-sep-2243)
- [Cache fields and cache hints](#cache-fields-and-cache-hints)
- [Tasks: deprecated wire vocabulary](#tasks-deprecated-wire-vocabulary)
- [Appendix: 2025-era vs 2026-era behavior matrix](#appendix-2025-era-vs-2026-era-behavior-matrix)
---
## Serving the 2026-07-28 revision
These entry points are documented in full in the server and client guides; this section
contextualizes them as the migration path.
### Client side: `versionNegotiation`
By default `Client.connect()` performs the same 2025 `initialize` handshake as v1.x,
byte for byte. To negotiate the 2026-07-28 era, opt in via `ClientOptions.versionNegotiation` —
see [client.md › Protocol version negotiation](../client.md#protocol-version-negotiation-2026-07-28-revision).
```typescript
const client = new Client({ name: 'my-client', version: '1.0.0' }, { versionNegotiation: { mode: 'auto' } });
await client.connect(transport);
client.getProtocolEra(); // 'modern' | 'legacy'
```
- **absent / `mode: 'legacy'`** (default) — today's behavior, no probe.
- **`mode: 'auto'`** — probe with `server/discover`; fall back to the 2025 handshake on
the same connection against a 2025-only server (one extra round trip).
- **`mode: { pin: '2026-07-28' }`** — modern only; no fallback, `connect()` rejects with
`SdkError(EraNegotiationFailed)` against a 2025-only server.
#### Probe policy
Failure semantics under `'auto'` are deliberately conservative but never silent about
infrastructure problems. Anything the probe does not positively recognize as modern
falls back to the legacy era — provided the supported-versions list still contains a
2025-era revision; with a modern-only list `connect()` rejects with
`SdkError(EraNegotiationFailed)` instead. A network outage rejects with a typed connect
error. Probe timeouts are **transport-aware**: on **stdio** a server that does not
answer within `timeoutMs` is treated as legacy and the client falls back to `initialize`
on the same stream (some legacy servers never respond to unknown pre-`initialize`
requests at all); on **HTTP** a probe timeout rejects with `SdkError(RequestTimeout)` —
a dead HTTP server is never misreported as legacy. One browser-specific exception: an
opaque CORS/preflight `TypeError` during the probe falls back to the legacy era, because
deployed 2025 servers commonly have CORS allow-lists that predate the 2026 headers.
```typescript
versionNegotiation: {
mode: 'auto',
probe: {
timeoutMs: 10_000, // default: the standard request timeout
maxRetries: 0 // default: no retries — governs timeout re-sends only
}
}
```
`maxRetries` governs timeout re-sends only (the spec-mandated `-32022` corrective
continuation — select-and-continue with a mutual version — is a separate negotiation step
and is never counted against it).
Once a modern era is negotiated the client auto-attaches the per-request `_meta`
envelope to every outgoing request and notification. A gateway/worker fleet can skip the
probe entirely with `client.connect(transport, { prior: persistedDiscoverResult })`.
### Server over HTTP: `createMcpHandler`
`createMcpHandler(factory)` from `@modelcontextprotocol/server` is the v2 HTTP entry
that serves 2026-07-28 per request — and, by default (`legacy: 'stateless'`), also
serves 2025-era traffic per request through the established stateless idiom. One
factory, one endpoint, both eras.
```typescript
import { createMcpHandler, McpServer } from '@modelcontextprotocol/server';
const handler = createMcpHandler(() => {
const server = new McpServer({ name: 'my-server', version: '1.0.0' }, { capabilities: { tools: {} } });
// register tools/resources/prompts once — the same factory backs both eras
return server;
});
// Web-standard runtimes: export default handler;
// Node frameworks: app.all('/mcp', toNodeHandler(handler)) from @modelcontextprotocol/node
```
A v1 stateless `StreamableHTTPServerTransport` hosting (`sessionIdGenerator: undefined`,
fresh transport per request) maps directly onto the default entry. An existing
**sessionful** v1 Streamable HTTP setup keeps serving 2025 clients by routing it in
front of a strict (`legacy: 'reject'`) entry with `isLegacyRequest(request)`:
```typescript
const modern = createMcpHandler(factory, { legacy: 'reject' });
export default {
async fetch(request: Request) {
if (await isLegacyRequest(request)) return myExistingLegacyHandler(request);
return modern.fetch(request);
}
};
```
`isLegacyRequest` returns `true` only for requests with no per-request `_meta` envelope
claim; route `false` traffic to the modern handler (a malformed modern claim is `false`
and answered `-32602` / `-32020` by the modern path). The handler is web-standards-only
(`{ fetch, close, notify, bus }`); on Node frameworks wrap once with
`toNodeHandler(handler, { onerror? })` from `@modelcontextprotocol/node`. The exported
`legacyStatelessFallback(factory)` is the same stateless 2025 serving as a standalone
fetch-shaped handler.
> **If you were on a v2 alpha:** `handler.node(req, res, body)` is gone — replace with
> `toNodeHandler(handler)` and add the `@modelcontextprotocol/node` import.
> `NodeIncomingMessageLike` / `NodeServerResponseLike` are now exported from
> `@modelcontextprotocol/node`, not `@modelcontextprotocol/server`.
### Server over stdio / long-lived connections: `serveStdio`
A hand-constructed `Server`/`McpServer` connected directly to a `StdioServerTransport`
serves only the 2025-era protocol — upgrading the SDK changes nothing about what it puts
on the wire. Serving 2026-07-28 (or both eras) on stdio goes through the
connection-pinned `serveStdio(() => buildServer())` entry from
`@modelcontextprotocol/server/stdio`; the opening exchange selects the connection's era,
and one factory instance is pinned per connection. See
[server.md › Serving the 2026-07-28 draft revision on stdio](../server.md#serving-the-2026-07-28-draft-revision-on-stdio).
To migrate an existing stdio server, replace
`await server.connect(new StdioServerTransport())` with
`serveStdio(() => buildServer())`. Pass `{ legacy: 'reject' }` to refuse 2025-era
openings. On 2026-pinned connections, `getClientCapabilities()` / `getClientVersion()`
return `undefined` (no `initialize` ever runs there) and handlers read per-request
identity from `ctx.mcpReq.envelope`; `getNegotiatedProtocolVersion()` reports the pinned
revision.
A client whose connection negotiated a modern era drops inbound server→client JSON-RPC
requests (the 2026 era has no such channel) instead of answering them; legacy-era
connections are unchanged.
### Client cancellation on Streamable HTTP
On a 2026-07-28 Streamable HTTP connection, aborting an in-flight client request
(`signal` / timeout) closes that request's SSE response stream — the spec cancellation
signal — instead of POSTing `notifications/cancelled`. Nothing to change in calling
code. 2025-era connections and stdio at any era still send `notifications/cancelled`.
Custom `Transport` implementations that open one underlying request per outbound message
and honor `TransportSendOptions.requestSignal` may opt in by declaring
`readonly hasPerRequestStream = true`.
### `ctx.mcpReq.log()` and the per-request `logLevel`
On a 2026-07-28 request, `ctx.mcpReq.log()` reads its level filter from the
`io.modelcontextprotocol/logLevel` `_meta` envelope key (the modern replacement for the
`logging/setLevel` RPC). When the key is **absent** the server emits no
`notifications/message` for that request — absence is opt-out, not "no filter". The SDK
`Client` does not auto-attach `logLevel`, so handler logs on a default 2026-era exchange
are silently suppressed until the client opts in.
---
## Replacing per-session state: `requestState`
The 2026-07-28 revision is **per request** — `createMcpHandler` builds a fresh server per
request and there is no `Mcp-Session-Id`. If your v1 server kept state keyed on the
session id (`ctx.sessionId` / `extra.sessionId`), the 2026 answer is `requestState`: an
opaque string the server returns with `inputRequired(...)` and the client echoes
byte-for-byte on the retry. Read it at `ctx.mcpReq.requestState`.
`requestState` round-trips through the client and is therefore **untrusted input** —
integrity-protect it (HMAC / AEAD over the payload, bound to principal, originating
method/parameters, and an expiry) and reject failed verification on re-entry. Configure
`ServerOptions.requestState.verify` and the seam runs it before the handler whenever
`requestState` is present (a thrown rejection answers `-32602` above the tool funnel).
The `createRequestStateCodec({ key, ttlSeconds?, bind? })` helper returns
`{ mint, verify }` — `mint` HMAC-SHA256-seals a JSON-serializable payload and `verify`
is exactly the function you assign to the hook. The codec is **signed, not encrypted**
(the client can base64url-decode the payload). See `examples/mrtr/server.ts` and
[Multi-round-trip requests](#multi-round-trip-requests) for the full handler shape.
---
## Auth on 2026-07-28
The 2026-07-28 specification's authorization requirements (RFC 9207 `iss` validation,
SEP-2352 credential isolation, SEP-2350 scope step-up, SEP-837/SEP-2207 DCR + TLS) are
implemented in v2 as **SDK-level opt-ins, not protocol-era gates** — they apply on every
era once enabled. The migration steps live in
[upgrade-to-v2.md › Auth](./upgrade-to-v2.md#auth). To be **2026-07-28-conformant**,
enable the spec-2026 opt-ins listed there: pass `iss` (or the callback `URLSearchParams`)
to `finishAuth`; round-trip the `issuer` stamp on stored credentials; implement
`discoveryState()`; and either keep `onInsufficientScope: 'reauthorize'` or handle
`InsufficientScopeError` yourself. Nothing in this section is era-switched at the wire
layer.
---
## Per-era wire codecs
The wire layer is split into per-revision codecs inside the (private, bundled) core: one
codec serves every 2025-era protocol version (2024-10-07 … 2025-11-25) and one serves
2026-07-28. The codec is selected by the negotiated protocol version, which is
**connection state** on the `Client`/`Server` instance (instances with no negotiated
version default to the 2025 era). An edge classification (`MessageExtraInfo.classification`)
no longer switches the era per message — it is validated against the instance era, and a
mismatch is rejected as an entry/routing error (`-32022 Unsupported protocol version`
for requests; drop + `onerror` for notifications).
Methods deleted by a protocol revision are **physically absent** from that era's
registry: an inbound `tasks/get` on a 2026-era connection gets `-32601` even if a
handler is registered, and sending an era-mismatched spec method (e.g. `server/discover`
toward a 2025-era peer, or any `tasks/*` method toward a 2026-era peer) throws
`SdkError(MethodNotSupportedByProtocolVersion)` before anything reaches the transport.
If you were on a v2 alpha and consumed wire schemas directly:
| v2-alpha pattern | Mechanical fix |
| --- | --- |
| parsing wire bytes with `EmptyResultSchema` that may carry `resultType` | strip `resultType` first (the schema now rejects it as an unknown key) |
| `specTypeSchemas` / `SpecTypeName` references to task message types or `RequestMetaEnvelope` | remove — these validators left the public set (the **types** remain importable) |
| `ClientRequest` / `ServerResult` / … aggregate types expected to include task members | use the individual deprecated `Task*` types — role aggregates are now the neutral (task-free) sets |
| relying on `isCallToolResult` to reject wire-only members | guards validate neutral shapes (loose passthrough); validate raw wire traffic with a transport-level parse |
The `resultType` / `EmptyResultSchema` / `specTypeSchemas` rules above have **no v1.x
impact** — these members did not exist before 2026-07-28. The neutral-model wire
tightening that **does** affect v1 code (`content` required, custom-handler `_meta`
passthrough, `specTypeSchemas` narrowing) is in
[upgrade-to-v2.md › Wire tightening](./upgrade-to-v2.md#wire-tightening-every-era).
> **If you were on a v2 alpha:** the 2026-07-28 draft error codes were renumbered:
> `HeaderMismatch` `-32001`→`-32020`, `MissingRequiredClientCapability` `-32003`→`-32021`,
> `UnsupportedProtocolVersion` `-32004`→`-32022`. No v1.x impact (these codes never
> existed in v1); v2-alpha code that hard-coded the old literals must update — prefer
> `ProtocolErrorCode.*` / `HEADER_MISMATCH_ERROR_CODE`.
---
## Wire-only members hidden from public types
The 2026-07-28 wire-level bookkeeping is handled internally and never reaches
application code: the `resultType` discrimination field, the reserved per-request
`_meta` envelope keys (`io.modelcontextprotocol/{protocolVersion,clientInfo,clientCapabilities,logLevel}`),
and the multi-round-trip retry fields (`inputResponses`, `requestState`).
- **`resultType` is gone from every public result type** (`Result`, `CallToolResult`,
`GetPromptResult`, …). The wire schemas keep parsing it, and the protocol layer
consumes it before results reach your code.
- **High-level methods return the named public types** (`client.callTool()` →
`Promise<CallToolResult>`, etc.). Handler return positions are unaffected.
- **Reserved envelope keys and retry fields appear in no public params/result type.**
The `RequestMetaEnvelope` type and the four `*_META_KEY` constants stay exported.
The protocol layer enforces the same boundary at runtime:
- **Envelope lift.** On inbound requests and notifications, the reserved
`io.modelcontextprotocol/*` keys are lifted out of `params._meta` before handlers run.
For requests the envelope is readable at `ctx.mcpReq.envelope`
(typed `Partial<RequestMetaEnvelope>`); for notifications there is no per-message
context, so lifted envelope keys are dropped. On requests only, `inputResponses` /
`requestState` are lifted from top-level params to `ctx.mcpReq.inputResponses` /
`ctx.mcpReq.requestState`; notification params are never touched.
- **Collision note for 2025-era peers.** The `_meta` lift is invisible to conforming
2025 traffic (the `io.modelcontextprotocol/` prefix is reserved in 2025-11-25 too).
The retry-field lift is the one collision: 2025-11-25 does not reserve the bare names
`inputResponses`/`requestState`, so a 2025 peer's **custom-method request** that uses
them as ordinary top-level params has them lifted out of `request.params` (still
readable at `ctx.mcpReq.inputResponses` / `ctx.mcpReq.requestState`).
- **Raw-first result discrimination.** On a 2026-era exchange, `'complete'` is consumed
and stripped; `'input_required'` is fulfilled by the client's auto-fulfilment driver;
any other kind rejects with `SdkError(UnsupportedResultType)` (kind in
`error.data.resultType`). On a 2025-era connection a foreign `resultType` is stripped
before validation. On a 2026-era exchange `resultType` is REQUIRED; an absent value is
a spec violation surfaced as a typed error.
**If you were on a v2 alpha** and read the wire shape directly:
| Pattern | Mechanical fix |
| --- | --- |
| `result.resultType` (typed read) | delete the read — the SDK consumes the field; results are complete when delivered |
| `Result['resultType']` type reference | remove; the member is no longer declared |
| return-type capture of `callTool` etc. | use the named public types (`CallToolResult`, `ListToolsResult`, …) |
`MessageExtraInfo.classification` is an optional carrier (`{ era, revision?, envelope? }`)
for transports that classify inbound messages at the edge; dispatch validates it against
the instance's negotiated era.
---
## Multi-round-trip requests
The 2026-07-28 revision removes the server→client JSON-RPC request channel. Servers
obtain client input (elicitation, sampling, roots) **in-band** by returning
`inputRequired(...)` from a `tools/call` / `prompts/get` / `resources/read` handler; the
client retries the original call with the responses.
| Handler serving 2026-07-28 requests | Mechanical fix |
| --- | --- |
| `await ctx.mcpReq.elicitInput({…})` / `requestSampling({…})` | `return inputRequired({ inputRequests: { id: inputRequired.elicit({…}) } })`; read `acceptedContent(ctx.mcpReq.inputResponses, 'id')` on re-entry |
| `throw new UrlElicitationRequiredError([…])` | `return inputRequired({ inputRequests: { id: inputRequired.elicitUrl({…}) } })` |
| handler shared across both eras | branch on the served era: keep the push-style call toward 2025-era requests, return `inputRequired(...)` toward 2026-07-28 requests |
`inputRequired` / `acceptedContent` / `InputRequiredSpec` are exported from
`@modelcontextprotocol/server`. On 2026-era requests the push-style APIs
(`ctx.mcpReq.send` of server→client requests, `ctx.mcpReq.elicitInput`,
`ctx.mcpReq.requestSampling`, instance-level `createMessage()`/`elicitInput()`/`listRoots()`/`ping()`)
fail with a typed local error before anything reaches the wire; their behavior toward
2025-era requests is unchanged.
`requestState` round-trips as an opaque, **untrusted** string — see
[Replacing per-session state: `requestState`](#replacing-per-session-state-requeststate)
for the sealing helper and verification hook.
**Client side — auto-fulfilment by default.** When a 2026-07-28 call answers
`input_required`, the client fulfils the embedded requests through the same handlers
registered with `setRequestHandler('elicitation/create' | 'sampling/createMessage' |
'roots/list', …)` and retries (fresh request id, `inputResponses`, byte-exact
`requestState` echo) up to `inputRequired.maxRounds` rounds (default 10). Configure or
opt out via `ClientOptions.inputRequired` (`{ autoFulfill: false }`); drive manually per
call with `allowInputRequired: true` plus `withInputRequired()`. Expect
`SdkError(InputRequiredRoundsExceeded)` when the cap is exhausted.
---
## `subscriptions/listen`
The 2026-07-28 revision delivers `tools/prompts/resources` `list_changed` and
`resources/updated` only on a `subscriptions/listen` stream the client opened — the
server never sends an un-requested notification type.
**Server side.** Nothing to register: the serving entries handle `subscriptions/listen`
themselves. `createMcpHandler` returns
`.notify.{toolsChanged, promptsChanged, resourcesChanged, resourceUpdated(uri)}` typed
publish sugar over an in-process bus (supply your own `ServerEventBus` for multi-process
deployments). On stdio, `serveStdio` routes the pinned instance's existing
`send*ListChanged()` calls onto the active subscriptions automatically. The 2025-era
unsolicited delivery model is unchanged on legacy connections.
**Client side.** `ClientOptions.listChanged` keeps working: on a 2026-07-28 connection
the SDK auto-opens a `subscriptions/listen` stream whose filter is the intersection of
the configured sub-options and the server-advertised `listChanged` capabilities, so the
same handlers fire on every published change. `client.listen(filter)` opens a stream
explicitly. `resources/subscribe` is 2025-only — on a 2026-07-28 connection, request
`notifications/resources/updated` via the `resourceSubscriptions` field of the listen
filter instead.
**Graceful close.** When the server closes the listen stream deliberately (entry
`close()`/shutdown), it sends the empty `subscriptions/listen` JSON-RPC result before
closing the stream; `McpSubscription.closed` resolves `'graceful'`. A stream close
without a result resolves `'remote'` and indicates an unexpected disconnect — re-listen
if you still want events.
---
## `Mcp-Param-*` and standard headers (SEP-2243)
On a 2026-07-28 connection over Streamable HTTP, `Client.callTool()` mirrors tool
arguments designated with `x-mcp-header` in the tool's `inputSchema` into
`Mcp-Param-{Name}` HTTP request headers (Base64-sentinel-encoded where needed), and
`createMcpHandler` rejects a `tools/call` whose `Mcp-Param-*` headers are missing for a
present body value, malformed, or disagree with the body — `400 Bad Request` with
JSON-RPC `-32020` (`HeaderMismatch`). The Streamable HTTP transport also emits the
`Mcp-Name` standard header on every modern-enveloped request, and `createMcpHandler`
validates the SEP-2243 standard headers (`MCP-Protocol-Version`, `Mcp-Method`,
`Mcp-Name`) against the body on the modern path with the same rejection.
**Modern-era exception** to the `SdkHttpError` mapping: on a modern-enveloped request,
an HTTP `400` whose body is a well-formed JSON-RPC error response addressed to the
pending request id is delivered in-band as a `ProtocolError` (so the `-32020` recovery
retry can fire). Legacy-era exchanges and generic HTTP failures still surface as
`SdkHttpError`.
Additive options: `CallToolRequestOptions.toolDefinition` (pass the tool definition
directly so mirroring and output-schema validation run without a prior `tools/list`),
`TransportSendOptions.headers` (per-request HTTP headers; reserved standard/auth header
names are skipped). Browser clients skip mirroring (dynamically named headers cannot be
statically allow-listed for credentialed CORS).
---
## Cache fields and cache hints
The 2026-07-28 revision requires `ttlMs` and `cacheScope` on the cacheable results.
When serving that revision, the SDK always emits both fields, defaulting to `ttlMs: 0`
and `cacheScope: 'private'` (the most conservative policy). To advertise a real cache
policy, set `ServerOptions.cacheHints` (per-operation) or `cacheHint` on a
`registerResource` metadata object; resolution is per field, most-specific author first.
2025-era responses never carry these fields.
---
## Tasks: deprecated wire vocabulary
The task **wire surface** defined by the 2025-11-25 protocol revision is still exported
for interoperability with peers on that revision: the task Zod schemas and inferred
types (`Task`, `TaskStatus`, `TaskMetadata`, `RelatedTaskMetadata`, `CreateTaskResult`,
`GetTask*`, `ListTasks*`, `CancelTask*`, `TaskStatusNotification*`,
`TaskAugmentedRequestParams`), the task members of the request/result/notification union
types, the `tasks` capability key, `isTaskAugmentedRequestParams`, and
`RELATED_TASK_META_KEY`. All are now `@deprecated` (importable wire vocabulary only;
removable at the major version that drops 2025-era support).
Task methods are excluded from the typed method maps: `RequestMethod` / `RequestTypeMap`
/ `ResultTypeMap` / `NotificationTypeMap` have no `tasks/*` or
`notifications/tasks/status` entries, so the method-keyed overloads of `request()`,
`ctx.mcpReq.send()`, `setRequestHandler()`, `setNotificationHandler()` reject task
methods at compile time. `ResultTypeMap['tools/call']` is plain `CallToolResult` (no
`| CreateTaskResult`); same for `sampling/createMessage` and `elicitation/create`. Where
task interop is genuinely required, use the explicit-schema custom-method form
(`request({ method: 'tasks/get', params }, GetTaskResultSchema)`). Inbound `tasks/*`
requests → `-32601`.
The experimental tasks **interception** layer is removed entirely — see
[upgrade-to-v2.md › Experimental tasks interception removed](./upgrade-to-v2.md#experimental-tasks-interception-removed).
---
## Appendix: 2025-era vs 2026-era behavior matrix
| Axis | 2025-era (2024-10-07 … 2025-11-25) | 2026-07-28 |
| --- | --- | --- |
| Server HTTP entry | `*StreamableHTTPServerTransport` | `createMcpHandler` (`legacy: 'stateless'` also serves 2025) |
| Server stdio entry | `server.connect(new StdioServerTransport())` | `serveStdio(factory)` (also serves 2025 unless `legacy: 'reject'`) |
| Client connect | `initialize` handshake | `server/discover` probe (`versionNegotiation`) |
| Client identity | `getClientCapabilities()` / `getClientVersion()` (initialize-scoped) | `ctx.mcpReq.envelope` (per request) |
| Server→client requests | `ctx.mcpReq.elicitInput` / `requestSampling`, instance `createMessage()` etc. | `return inputRequired(...)` from handler |
| Change notifications | unsolicited `list_changed` / `resources/updated` | `subscriptions/listen` stream |
| Client cancellation (Streamable HTTP) | POST `notifications/cancelled` | close the request's SSE response stream |
| `ctx.mcpReq.log()` level filter | session-scoped `logging/setLevel` | per-request `_meta.logLevel` envelope key (absent = opt-out) |
| `400` JSON-RPC error body | `SdkHttpError` | `ProtocolError` (in-band) |
| Era-mismatched spec method (outbound) | n/a | `SdkError(MethodNotSupportedByProtocolVersion)` |

File diff suppressed because it is too large Load diff