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:
parent
e4227d135c
commit
1823aae891
3 changed files with 69131 additions and 9001 deletions
44
docs/migration/index.md
Normal file
44
docs/migration/index.md
Normal 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)
|
||||
443
docs/migration/support-2026-07-28.md
Normal file
443
docs/migration/support-2026-07-28.md
Normal 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)` |
|
||||
1058
docs/migration/upgrade-to-v2.md
Normal file
1058
docs/migration/upgrade-to-v2.md
Normal file
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue