Skip to content

Commit b96a94c

Browse files
docs(migration): close guide gaps surfaced by real v1-to-v2 migrations (#2382)
1 parent 9f8ba61 commit b96a94c

5 files changed

Lines changed: 412 additions & 184 deletions

File tree

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
---
2+
---
3+
4+
Docs-only: migration-guide corrections from real v1-to-v2 migrations. No package changes.

docs/migration/index.md

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
1+
---
2+
title: Migration Guides
3+
children:
4+
- ./upgrade-to-v2.md
5+
- ./support-2026-07-28.md
6+
---
7+
18
# MCP TypeScript SDK — Migration Guides
29

310
Pick the guide for your starting point.
@@ -12,9 +19,12 @@ v2 packages (`@modelcontextprotocol/client`, `@modelcontextprotocol/server`, …
1219
Start by running the codemod:
1320

1421
```bash
15-
npx @modelcontextprotocol/codemod@alpha v1-to-v2 ./src
22+
npx @modelcontextprotocol/codemod@alpha v1-to-v2 .
1623
```
1724

25+
Run it at the package root (`.`) — real projects import the SDK from `test/`,
26+
`scripts/`, and fixtures too, and those rewrites are missed when you point it at `./src`.
27+
1828
The codemod handles most mechanical renames. The guide covers what it can't. The
1929
codemod handles the v1→v2 SDK surface upgrade only — adopting the 2026-07-28 protocol
2030
revision (`createMcpHandler`, multi-round-trip requests, `versionNegotiation`) is

docs/migration/support-2026-07-28.md

Lines changed: 77 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,19 @@
1+
---
2+
title: Supporting protocol revision 2026-07-28
3+
---
4+
15
# Supporting protocol revision 2026-07-28
26

37
This guide is for code **already on the v2 packages** that wants to speak the 2026-07-28
48
protocol revision — and for code written against an earlier **v2 alpha** that read
59
wire-only members directly. If you are on `@modelcontextprotocol/sdk` (v1.x), start with
610
[upgrade-to-v2.md](./upgrade-to-v2.md) instead.
711

12+
> **Schema artifact:** until the revision is finalized, the spec repository publishes
13+
> the 2026-07-28 schema under `schema/draft/` — there is no `schema/2026-07-28/`
14+
> directory yet. Tooling that vendors per-revision schema artifacts should track
15+
> `draft/` and note the divergence.
16+
817
Nothing in v2 puts a 2026-07-28 byte on the wire by default: a hand-constructed
918
`Client` / `Server` / `McpServer` keeps speaking the 2025-era protocol it was written
1019
for. Serving or speaking 2026-07-28 is always an explicit opt-in via one of the entries
@@ -49,6 +58,14 @@ client.getProtocolEra(); // 'modern' | 'legacy'
4958
- **`mode: { pin: '2026-07-28' }`** — modern only; no fallback, `connect()` rejects with
5059
`SdkError(EraNegotiationFailed)` against a 2025-only server.
5160

61+
`ProtocolOptions.supportedProtocolVersions` — the same option that pins what the legacy
62+
`initialize` handshake offers (see
63+
[upgrade-to-v2.md › Client connection & dispatch](./upgrade-to-v2.md#client-connection--dispatch))
64+
— shapes `'auto'`: the modern candidates are the option's modern entries (when it lists
65+
any; otherwise the SDK's default modern set), and legacy fallback is available only if
66+
the list has a pre-2026 entry. A `{ pin }` is honored as given — it must name a modern
67+
revision but is not checked against the list.
68+
5269
#### Probe policy
5370

5471
Failure semantics under `'auto'` are deliberately conservative but never silent about
@@ -78,8 +95,19 @@ versionNegotiation: {
7895
continuation — select-and-continue with a mutual version — is a separate negotiation step
7996
and is never counted against it).
8097

81-
Once a modern era is negotiated the client auto-attaches the per-request `_meta`
82-
envelope to every outgoing request and notification. A gateway/worker fleet can skip the
98+
**Who should not default to `'auto'`:** spawn-per-invocation CLI and debugging tools.
99+
On stdio, a legacy server that never answers unknown pre-`initialize` requests stalls
100+
`connect()` for the full probe timeout before falling back; and the probe round trip
101+
changes recorded transcripts/raw logs, which matters for tools whose value is
102+
byte-stable observation. Such tools should keep the default and expose `'auto'` /
103+
a pin as an explicit flag.
104+
105+
The probe request itself already carries the per-request `_meta` envelope
106+
(`io.modelcontextprotocol/protocolVersion`, `clientInfo`, `clientCapabilities`) —
107+
**before** the era is known. Once a modern era is negotiated the client auto-attaches
108+
the envelope to every outgoing request and notification. Tooling that classifies
109+
traffic must not treat "saw an envelope" as "modern era negotiated": the legacy-fallback
110+
path also begins with one enveloped probe. A gateway/worker fleet can skip the
83111
probe entirely with `client.connect(transport, { prior: persistedDiscoverResult })`.
84112

85113
### Server over HTTP: `createMcpHandler`
@@ -151,6 +179,22 @@ A client whose connection negotiated a modern era drops inbound server→client
151179
requests (the 2026 era has no such channel) instead of answering them; legacy-era
152180
connections are unchanged.
153181

182+
### In-process testing
183+
184+
There is no in-memory serving entry — `InMemoryTransport.createLinkedPair()` connects
185+
2025-era instances only. To exercise 2026-07-28 behavior in tests without sockets,
186+
drive `createMcpHandler` directly through its fetch function:
187+
188+
```typescript
189+
const handler = createMcpHandler(buildServer);
190+
const transport = new StreamableHTTPClientTransport(new URL('http://test.local/mcp'), {
191+
fetch: (url, init) => handler.fetch(new Request(url, init))
192+
});
193+
```
194+
195+
The URL is never dialed — `handler.fetch` serves the request in-process. For stdio-era
196+
coverage, spawn `serveStdio` as a child process.
197+
154198
### Client cancellation on Streamable HTTP
155199

156200
On a 2026-07-28 Streamable HTTP connection, aborting an in-flight client request
@@ -227,12 +271,12 @@ toward a 2025-era peer, or any `tasks/*` method toward a 2026-era peer) throws
227271

228272
If you were on a v2 alpha and consumed wire schemas directly:
229273

230-
| v2-alpha pattern | Mechanical fix |
231-
| --- | --- |
232-
| parsing wire bytes with `EmptyResultSchema` that may carry `resultType` | strip `resultType` first (the schema now rejects it as an unknown key) |
233-
| `specTypeSchemas` / `SpecTypeName` references to task message types or `RequestMetaEnvelope` | remove — these validators left the public set (the **types** remain importable) |
234-
| `ClientRequest` / `ServerResult` / … aggregate types expected to include task members | use the individual deprecated `Task*` types — role aggregates are now the neutral (task-free) sets |
235-
| relying on `isCallToolResult` to reject wire-only members | guards validate neutral shapes (loose passthrough); validate raw wire traffic with a transport-level parse |
274+
| v2-alpha pattern | Mechanical fix |
275+
| -------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- |
276+
| parsing wire bytes with `EmptyResultSchema` that may carry `resultType` | strip `resultType` first (the schema now rejects it as an unknown key) |
277+
| `specTypeSchemas` / `SpecTypeName` references to task message types or `RequestMetaEnvelope` | remove — these validators left the public set (the **types** remain importable) |
278+
| `ClientRequest` / `ServerResult` / … aggregate types expected to include task members | use the individual deprecated `Task*` types — role aggregates are now the neutral (task-free) sets |
279+
| relying on `isCallToolResult` to reject wire-only members | guards validate neutral shapes (loose passthrough); validate raw wire traffic with a transport-level parse |
236280

237281
The `resultType` / `EmptyResultSchema` / `specTypeSchemas` rules above have **no v1.x
238282
impact** — these members did not exist before 2026-07-28. The neutral-model wire
@@ -258,6 +302,10 @@ and the multi-round-trip retry fields (`inputResponses`, `requestState`).
258302
- **`resultType` is gone from every public result type** (`Result`, `CallToolResult`,
259303
`GetPromptResult`, …). The wire schemas keep parsing it, and the protocol layer
260304
consumes it before results reach your code.
305+
- **`DiscoverResult` strips its cache fields too.** `ttlMs` / `cacheScope` on
306+
`server/discover` are wire-only — consumed by the client's response-cache layer and
307+
absent from the public `DiscoverResult` type returned by `getDiscoverResult()`.
308+
Tooling that displays the server's advertised cache policy must parse raw frames.
261309
- **High-level methods return the named public types** (`client.callTool()`
262310
`Promise<CallToolResult>`, etc.). Handler return positions are unaffected.
263311
- **Reserved envelope keys and retry fields appear in no public params/result type.**
@@ -287,11 +335,11 @@ The protocol layer enforces the same boundary at runtime:
287335

288336
**If you were on a v2 alpha** and read the wire shape directly:
289337

290-
| Pattern | Mechanical fix |
291-
| --- | --- |
292-
| `result.resultType` (typed read) | delete the read — the SDK consumes the field; results are complete when delivered |
293-
| `Result['resultType']` type reference | remove; the member is no longer declared |
294-
| return-type capture of `callTool` etc. | use the named public types (`CallToolResult`, `ListToolsResult`, …) |
338+
| Pattern | Mechanical fix |
339+
| -------------------------------------- | --------------------------------------------------------------------------------- |
340+
| `result.resultType` (typed read) | delete the read — the SDK consumes the field; results are complete when delivered |
341+
| `Result['resultType']` type reference | remove; the member is no longer declared |
342+
| return-type capture of `callTool` etc. | use the named public types (`CallToolResult`, `ListToolsResult`, …) |
295343

296344
`MessageExtraInfo.classification` is an optional carrier (`{ era, revision?, envelope? }`)
297345
for transports that classify inbound messages at the edge; dispatch validates it against
@@ -306,11 +354,11 @@ obtain client input (elicitation, sampling, roots) **in-band** by returning
306354
`inputRequired(...)` from a `tools/call` / `prompts/get` / `resources/read` handler; the
307355
client retries the original call with the responses.
308356

309-
| Handler serving 2026-07-28 requests | Mechanical fix |
310-
| --- | --- |
357+
| Handler serving 2026-07-28 requests | Mechanical fix |
358+
| ------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------- |
311359
| `await ctx.mcpReq.elicitInput({…})` / `requestSampling({…})` | `return inputRequired({ inputRequests: { id: inputRequired.elicit({…}) } })`; read `acceptedContent(ctx.mcpReq.inputResponses, 'id')` on re-entry |
312-
| `throw new UrlElicitationRequiredError([…])` | `return inputRequired({ inputRequests: { id: inputRequired.elicitUrl({…}) } })` |
313-
| 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 |
360+
| `throw new UrlElicitationRequiredError([…])` | `return inputRequired({ inputRequests: { id: inputRequired.elicitUrl({…}) } })` |
361+
| 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 |
314362

315363
`inputRequired` / `acceptedContent` / `InputRequiredSpec` are exported from
316364
`@modelcontextprotocol/server`. On 2026-era requests the push-style APIs
@@ -429,15 +477,15 @@ The experimental tasks **interception** layer is removed entirely — see
429477

430478
## Appendix: 2025-era vs 2026-era behavior matrix
431479

432-
| Axis | 2025-era (2024-10-07 … 2025-11-25) | 2026-07-28 |
433-
| --- | --- | --- |
434-
| Server HTTP entry | `*StreamableHTTPServerTransport` | `createMcpHandler` (`legacy: 'stateless'` also serves 2025) |
435-
| Server stdio entry | `server.connect(new StdioServerTransport())` | `serveStdio(factory)` (also serves 2025 unless `legacy: 'reject'`) |
436-
| Client connect | `initialize` handshake | `server/discover` probe (`versionNegotiation`) |
437-
| Client identity | `getClientCapabilities()` / `getClientVersion()` (initialize-scoped) | `ctx.mcpReq.envelope` (per request) |
438-
| Server→client requests | `ctx.mcpReq.elicitInput` / `requestSampling`, instance `createMessage()` etc. | `return inputRequired(...)` from handler |
439-
| Change notifications | unsolicited `list_changed` / `resources/updated` | `subscriptions/listen` stream |
440-
| Client cancellation (Streamable HTTP) | POST `notifications/cancelled` | close the request's SSE response stream |
441-
| `ctx.mcpReq.log()` level filter | session-scoped `logging/setLevel` | per-request `_meta.logLevel` envelope key (absent = opt-out) |
442-
| `400` JSON-RPC error body | `SdkHttpError` | `ProtocolError` (in-band) |
443-
| Era-mismatched spec method (outbound) | n/a | `SdkError(MethodNotSupportedByProtocolVersion)` |
480+
| Axis | 2025-era (2024-10-07 … 2025-11-25) | 2026-07-28 |
481+
| ------------------------------------- | ----------------------------------------------------------------------------- | ------------------------------------------------------------------ |
482+
| Server HTTP entry | `*StreamableHTTPServerTransport` | `createMcpHandler` (`legacy: 'stateless'` also serves 2025) |
483+
| Server stdio entry | `server.connect(new StdioServerTransport())` | `serveStdio(factory)` (also serves 2025 unless `legacy: 'reject'`) |
484+
| Client connect | `initialize` handshake | `server/discover` probe (`versionNegotiation`) |
485+
| Client identity | `getClientCapabilities()` / `getClientVersion()` (initialize-scoped) | `ctx.mcpReq.envelope` (per request) |
486+
| Server→client requests | `ctx.mcpReq.elicitInput` / `requestSampling`, instance `createMessage()` etc. | `return inputRequired(...)` from handler |
487+
| Change notifications | unsolicited `list_changed` / `resources/updated` | `subscriptions/listen` stream |
488+
| Client cancellation (Streamable HTTP) | POST `notifications/cancelled` | close the request's SSE response stream |
489+
| `ctx.mcpReq.log()` level filter | session-scoped `logging/setLevel` | per-request `_meta.logLevel` envelope key (absent = opt-out) |
490+
| `400` JSON-RPC error body | `SdkHttpError` | `ProtocolError` (in-band) |
491+
| Era-mismatched spec method (outbound) | n/a | `SdkError(MethodNotSupportedByProtocolVersion)` |

0 commit comments

Comments
 (0)