Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -201,9 +201,9 @@ The `ctx` parameter in handlers provides a structured context:
- `http?`: HTTP transport info (undefined for stdio)
- `authInfo?`: Validated auth token info

**`ServerContext`** extends `BaseContext.mcpReq` and `BaseContext.http?` via type intersection:
**`ServerContext`** extends `BaseContext.mcpReq` and `BaseContext.http?` via type intersection, and adds a top-level `clientCapabilities?` field:

- `mcpReq` adds: `log(level, data, logger?)`, `elicitInput(params, options?)`, `requestSampling(params, options?)`
- `mcpReq` adds: `log(level, data, logger?)`, `elicitInput(params, options?)`, `requestSampling(params, options?)`, `listRoots(options?)`
- `http?` adds: `req?` (HTTP request info), `closeSSE?`, `closeStandaloneSSE?`

**`ClientContext`** is currently identical to `BaseContext`.
Expand Down
34 changes: 32 additions & 2 deletions docs/migration-SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -429,6 +429,9 @@
| `ctx.mcpReq.log(level, data, logger?)` | Send log notification (respects client's level filter) | `server.sendLoggingMessage(...)` from within handler |
| `ctx.mcpReq.elicitInput(params, options?)` | Elicit user input (form or URL) | `server.elicitInput(...)` from within handler |
| `ctx.mcpReq.requestSampling(params, options?)` | Request LLM sampling from client | `server.createMessage(...)` from within handler |
| `ctx.mcpReq.listRoots(options?)` | List client roots | `server.listRoots(...)` from within handler |

Check warning on line 432 in docs/migration-SKILL.md

View check run for this annotation

Claude / Claude Code Review

Docs show listRoots(options?) but actual signature is listRoots(params?, options?)

The §10 table row added in this PR documents the convenience method as `ctx.mcpReq.listRoots(options?)`, and the matching CLAUDE.md line (also updated here) says `listRoots(options?)` — but the actual signature is `listRoots(params?, options?)`. A reader passing a `RequestOptions` object as the first argument would silently bind it to `params` and lose the timeout/signal; small fix to `(params?, options?)` in both files.
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 The §10 table row added in this PR documents the convenience method as ctx.mcpReq.listRoots(options?), and the matching CLAUDE.md line (also updated here) says listRoots(options?) — but the actual signature is listRoots(params?, options?). A reader passing a RequestOptions object as the first argument would silently bind it to params and lose the timeout/signal; small fix to (params?, options?) in both files.

Extended reasoning...

What the bug is

The ServerContext convenience methods table in §10 of docs/migration-SKILL.md (the listRoots row was added in this PR via commit 4e002f6) documents the method as:

| ctx.mcpReq.listRoots(options?) | List client roots | server.listRoots(...) from within handler |

CLAUDE.md's "Request Handler Context" section (also touched in this PR) says the same: mcpReq adds: ... listRoots(options?).

But the actual declared and wired signature takes params? first, then options?:

  • packages/core/src/shared/protocol.ts:246listRoots: (params?: ListRootsRequest['params'], options?: RequestOptions) => Promise<ListRootsResult>
  • packages/server/src/server/server.ts:566 — wired as listRoots: (params, options) => this.listRoots(params, options)
  • packages/server/src/server/server.ts:932 — the deprecated top-level Server.listRoots is also async listRoots(params?, options?)

Why it's also internally inconsistent

The sibling rows in the same §10 table correctly show elicitInput(params, options?) and requestSampling(params, options?). So listRoots(options?) contradicts both the implementation and its neighbors. It also contradicts the §15 mapping table in the same file, which shows .listRoots( → ctx.mcpReq.listRoots( as a parameter-preserving rewrite (i.e., it inherits the (params?, options?) signature).

What the impact would be

migration-SKILL.md is explicitly the LLM-optimized table for mechanical/programmatic rewrites, so signature accuracy matters more here than in prose docs. A migrator (human or LLM) following the documented signature and writing ctx.mcpReq.listRoots({ timeout: 5000, signal }) would have that object bind to params (typed as ListRootsRequest['params'], which is { _meta?: ... } | undefined) instead of options. The timeout and abort signal would silently not apply. TypeScript may or may not catch this depending on whether the RequestOptions shape is excess-property compatible with { _meta?: ... } — but even when caught, the doc is sending the reader to the wrong place.

Step-by-step proof

  1. A migrator reads the §10 table: ctx.mcpReq.listRoots(options?).
  2. They write await ctx.mcpReq.listRoots({ timeout: 30_000 }) expecting a 30s timeout.
  3. The actual signature is (params?, options?). The { timeout: 30_000 } object is bound to the params positional slot.
  4. options is undefined. The default request timeout applies instead of the requested 30s.
  5. The params object is forwarded as the roots/list request params — at best a no-op, at worst a malformed request body if validation is strict.

How to fix

Change to ctx.mcpReq.listRoots(params?, options?) in:

  • docs/migration-SKILL.md §10 table (line ~432)
  • CLAUDE.md line ~206 (mcpReq adds: list)

This brings both in line with the implementation and with the sibling rows elicitInput(params, options?) and requestSampling(params, options?).


`ServerContext` also adds a top-level `clientCapabilities?` field. See section 15 for the both-protocols mapping.

## 11. Schema parameter removed from `request()`, `send()`, and `callTool()` (spec methods)

Expand Down Expand Up @@ -522,7 +525,33 @@
- AJV (Node.js): `import { AjvJsonSchemaValidator } from '@modelcontextprotocol/server';`
- CF Worker: `import { CfWorkerJsonSchemaValidator } from '@modelcontextprotocol/server/validators/cf-worker';`

## 15. Migration Steps (apply in this order)
## 15. 2026-06 Stateless Support (SEP-2575/2567/2322)

`Server`/`Client` are the same classes (still extend `Protocol`); 2026-06 support is additive. `Client.connect()` auto-probes `server/discover` and falls back to legacy `initialize`. `setRequestHandler` is unchanged.

**Prefer `ctx.mcpReq.*` inside handlers** (works under both protocols). Inside tool/prompt/resource handler bodies where `ctx` (the handler's second argument) is in scope, replace `<expr>.server.X()` (or `server.X()` on a low-level `Server`) with `ctx.mcpReq.X()`. The `<expr>` prefix varies (`mcpServer.server`, `this.server`, just `server`), so search for `.X(` and rewrite by hand:

| Find calls to | Replace with |
| --- | --- |
| `.createMessage(` | `ctx.mcpReq.requestSampling(` |
| `.elicitInput(` | `ctx.mcpReq.elicitInput(` |
| `.listRoots(` | `ctx.mcpReq.listRoots(` |
Comment thread
claude[bot] marked this conversation as resolved.
| `.sendLoggingMessage({ level, data, logger })` | `ctx.mcpReq.log(level, data, logger)` |
| `.getClientCapabilities()` | `ctx.clientCapabilities` |

All five rows are the **both-protocols** path, not 2026-only: `ctx.mcpReq.listRoots()` and `ctx.clientCapabilities` work identically against pre-2026 and 2026-06 clients.

Add `ctx` to the handler signature if not already present. For tools with an `inputSchema`: `async (args) =>` → `async (args, ctx) =>`. For tools WITHOUT an `inputSchema`: `async () =>` → `async ctx =>` (single parameter).

The top-level `server.createMessage()` etc. still work with a connected pre-2026 client; this migration is recommended but not required.

`ctx.mcpReq.requestSampling` keeps the same overload narrowing as `server.createMessage`: when `params.tools` is statically present the result type is `CreateMessageResultWithTools`; when statically absent it is `CreateMessageResult`. If `tools` is conditional at the call site, the result is the union; add a runtime `Array.isArray(result.content)` check before indexing.

MRTR via `InputRequiredError` works for handlers registered via `setRequestHandler`; `fallbackRequestHandler` is not wrapped by middleware (matches pre-existing behavior).

For tests that exercise pre-2026 connection-model behavior, construct the test client with `supportedProtocolVersions` filtered to pre-2026 versions only.

## 16. Migration Steps (apply in this order)

1. Update `package.json`: `npm uninstall @modelcontextprotocol/sdk`, install the appropriate v2 packages
2. Replace all imports from `@modelcontextprotocol/sdk/...` using the import mapping tables (sections 3-4), including `StreamableHTTPServerTransport` → `NodeStreamableHTTPServerTransport`
Expand All @@ -534,4 +563,5 @@
8. If using server SSE transport, migrate to Streamable HTTP
9. If using server auth from the SDK: RS helpers (`requireBearerAuth`, `mcpAuthMetadataRouter`) → `@modelcontextprotocol/express`; AS helpers → external IdP/OAuth library
10. If relying on `listTools()`/`listPrompts()`/etc. throwing on missing capabilities, set `enforceStrictCapabilities: true`
11. Verify: build with `tsc` / run tests
11. Inside tool/prompt/resource handlers, replace `server.createMessage`/`elicitInput`/`listRoots`/`sendLoggingMessage` with `ctx.mcpReq.*` per section 15
12. Verify: build with `tsc` / run tests
44 changes: 43 additions & 1 deletion docs/migration.md
Original file line number Diff line number Diff line change
Expand Up @@ -645,7 +645,7 @@
});
```

These replace the pattern of calling `server.sendLoggingMessage()`, `server.createMessage()`, and `server.elicitInput()` from within handlers.
These replace the pattern of calling `server.sendLoggingMessage()`, `server.createMessage()`, `server.elicitInput()`, and `server.listRoots()` from within handlers. `ctx.clientCapabilities` likewise replaces `server.getClientCapabilities()`.

### Error hierarchy refactoring

Expand Down Expand Up @@ -869,6 +869,48 @@

There is no migration path for the removed surface; it was always `@experimental`. Under SEP-2663, tasks reattach via a `DispatchMiddleware` (`mcp.use(tasksPlugin({ store }))`) and handlers read task context from `ctx.ext.task` instead of `ctx.task`.

## 2026-06 Stateless Protocol Support (SEP-2575, SEP-2567, SEP-2322)

`Server` and `Client` now support the 2026-06 stateless connection model alongside the existing pre-2026 model. They remain the same classes (still extending `Protocol`); the new behavior is additive.

### What changed

- **`Client.connect()` auto-probes.** On connect, the client sends `server/discover` via the transport's `sendAndReceive` path. If the server responds, the client operates in stateless mode; typed methods (`callTool`, `listTools`, etc.) route via `sendAndReceive` and the MRTR loop. If discover fails (server doesn't support it, transport doesn't have `sendAndReceive`), the client falls back to the legacy `initialize` handshake. Existing code works unchanged.
- **`Server` gained a stateless dispatch path.** `server.statelessHandlers()` returns `{dispatch, listen}` for transports to call. `connect(transport)` wires this automatically via `transport.setStatelessHandlers?.()`. Handlers registered with `setRequestHandler` serve both paths.
- **`handleHttp(server, opts)`** is a new Fetch-API entry point: one shared `Server` instance, no `Transport`, no `connect()`. Returns `(Request) => Promise<Response>`. See `examples/server/src/honoWebStandardStreamableHttp.ts`.
- **`client.subscribe(filter)`** opens a `subscriptions/listen` stream for list-changed and resource-updated notifications (the 2026-06 replacement for unsolicited notifications and `resources/subscribe`).
- **`Transport` interface gained two optional methods:** `setStatelessHandlers?(handlers)` (server side) and `sendAndReceive?(req, opts?)` (client side). Implement these in custom transports to support 2026-06.

### Prefer `ctx.mcpReq.*` for server-to-client interactions

Inside a tool/prompt/resource handler, use `ctx.mcpReq.{elicitInput, requestSampling, listRoots, log}` instead of the top-level `server.elicitInput()` / `server.createMessage()` / `server.listRoots()` / `server.sendLoggingMessage()`. The `ctx.mcpReq.*` form works under **both** protocols: with a pre-2026 client it sends a real request; with a 2026-06 client it returns an `input_required` result and the client retries with the response embedded (SEP-2322 MRTR).

Check warning on line 886 in docs/migration.md

View check run for this annotation

Claude / Claude Code Review

MRTR prose incorrectly attributes input_required/retry behavior to ctx.mcpReq.log

The prose in the new "Prefer `ctx.mcpReq.*` for server-to-client interactions" section lists `ctx.mcpReq.{elicitInput, requestSampling, listRoots, log}` and then states the `ctx.mcpReq.*` form "sends a real request" (pre-2026) and "returns an `input_required` result and the client retries" (2026-06 MRTR) — but `log` is a one-way `notifications/message` notification, not a request, and never participates in MRTR (and `ctx.clientCapabilities` is a synchronous property read). Suggest scoping the re
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 The prose in the new "Prefer ctx.mcpReq.* for server-to-client interactions" section lists ctx.mcpReq.{elicitInput, requestSampling, listRoots, log} and then states the ctx.mcpReq.* form "sends a real request" (pre-2026) and "returns an input_required result and the client retries" (2026-06 MRTR) — but log is a one-way notifications/message notification, not a request, and never participates in MRTR (and ctx.clientCapabilities is a synchronous property read). Suggest scoping the request/MRTR clause to elicitInput/requestSampling/listRoots and adding a separate clause noting log streams a notification under both protocols.

Extended reasoning...

What the bug is. The new "Prefer ctx.mcpReq.* for server-to-client interactions" subsection in docs/migration.md reads:

Inside a tool/prompt/resource handler, use ctx.mcpReq.{elicitInput, requestSampling, listRoots, log} instead of the top-level … The ctx.mcpReq.* form works under both protocols: with a pre-2026 client it sends a real request; with a 2026-06 client it returns an input_required result and the client retries with the response embedded (SEP-2322 MRTR).

That sentence enumerates four members and then describes them all with request/MRTR semantics. The description is correct for elicitInput, requestSampling, and listRoots, but wrong for log (and not really applicable to getClientCapabilities()/ctx.clientCapabilities, which is a synchronous property read with no wire interaction at all).

Why log does not fit. ctx.mcpReq.log is a one-way notification, not a request, under both protocols:

  • Under pre-2026, ctx.mcpReq.log is wired to server.sendLoggingMessage(), which emits a notifications/message JSON-RPC notification. There is no request ID, no response, and no retry.
  • Under the 2026-06 stateless path, the JSDoc for _buildDispatchServerContext in packages/server/src/server/server.ts (around lines 380–385) explicitly distinguishes: "notify/log go out via dctx.notify; … elicitInput/requestSampling/listRoots are MRTR throw-then-cache." The implementation a few lines later shows log filtering on the level and calling notify({method: 'notifications/message', …}) — it never calls mrtrOrThrow(), never throws InputRequiredError, and never produces an input_required result for the client to retry.

So the prose's blanket claim — "sends a real request" / "returns an input_required result and the client retries" — is factually wrong for one of the four named methods.

How a reader would be misled. A migrator reading this paragraph and looking at the comparison table immediately below it would reasonably expect every row to behave the same way. Concretely: someone porting a tool that currently calls server.sendLoggingMessage() to ctx.mcpReq.log() and serving 2026-06 clients might (a) wrap the call expecting an input_required-driven retry, or (b) wonder why their stateless server never sees a follow-up retry carrying the "response" to a log line. Logging is fire-and-forget; the docs as written attribute MRTR mechanics to it.

Step-by-step proof.

  1. Open packages/server/src/server/server.ts and find _buildDispatchServerContext. The leading comment states: "notify/log go out via dctx.notify; send throws (no push channel under stateless); elicitInput/requestSampling/listRoots are MRTR throw-then-cache."
  2. The log implementation calls notify({ method: 'notifications/message', params: { level, data, logger } }) — a notification, no MRTR.
  3. The listRoots implementation calls mrtrOrThrow(...) — that one is MRTR.
  4. The docs/migration.md prose at the "Prefer ctx.mcpReq.*" subsection lists all four of elicitInput, requestSampling, listRoots, log and then describes the ctx.mcpReq.* form with request/MRTR semantics, contradicting steps 1–3 for log.

(The verifiers also checked docs/migration-SKILL.md §15; that section says only "works under both protocols" without the "sends a real request / input_required" phrasing, so the misattribution is confined to migration.md.)

How to fix. Scope the request/MRTR sentence to the three methods that actually participate, and add a short clause for log. For example:

Inside a tool/prompt/resource handler, use ctx.mcpReq.{elicitInput, requestSampling, listRoots, log} instead of the top-level equivalents. For elicitInput/requestSampling/listRoots, the ctx.mcpReq.* form works under both protocols: with a pre-2026 client it sends a real request; with a 2026-06 client it returns an input_required result and the client retries with the response embedded (SEP-2322 MRTR). ctx.mcpReq.log streams a notifications/message notification under both protocols (over the open connection pre-2026; over the response stream under 2026-06).

This is a documentation-only precision issue with no runtime impact, so it's a nit.


| Top-level (pre-2026 only) | Handler-context (both protocols) |
| --- | --- |
| `server.createMessage(params)` | `ctx.mcpReq.requestSampling(params)` |
| `server.elicitInput(params)` | `ctx.mcpReq.elicitInput(params)` |
| `server.listRoots()` | `ctx.mcpReq.listRoots()` |
| `server.sendLoggingMessage({level, data, logger})` | `ctx.mcpReq.log(level, data, logger)` |
| `server.getClientCapabilities()` | `ctx.clientCapabilities` |

`ctx.mcpReq.listRoots()` and `ctx.clientCapabilities` work under **both** protocols, not just 2026-06.

The top-level methods still exist and work when a pre-2026 client is connected. They are not removed.

Check warning on line 898 in docs/migration.md

View check run for this annotation

Claude / Claude Code Review

Migration docs omit that top-level server.* methods are @deprecated

The migration prose says the top-level `server.createMessage()`/`elicitInput()`/`listRoots()`/`sendLoggingMessage()`/`getClientCapabilities()` methods "still exist and work … They are not removed" (and §15 of `migration-SKILL.md` says "this migration is recommended but not required"), but the implementation marks all of them `@deprecated` in `packages/server/src/server/server.ts`. Mention the deprecation so a reader who chooses not to migrate isn't surprised by IDE strikethrough/lint warnings on
Comment on lines +896 to +898
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 The migration prose says the top-level server.createMessage()/elicitInput()/listRoots()/sendLoggingMessage()/getClientCapabilities() methods "still exist and work … They are not removed" (and §15 of migration-SKILL.md says "this migration is recommended but not required"), but the implementation marks all of them @deprecated in packages/server/src/server/server.ts. Mention the deprecation so a reader who chooses not to migrate isn't surprised by IDE strikethrough/lint warnings on code the guide just framed as fine to keep.

Extended reasoning...

What the bug is. Both new passages added by this PR — docs/migration.md ("The top-level methods still exist and work when a pre-2026 client is connected. They are not removed.") and docs/migration-SKILL.md §15 ("The top-level server.createMessage() etc. still work with a connected pre-2026 client; this migration is recommended but not required.") — describe the top-level server.* methods as merely an optional migration target. They omit the material fact that each of those methods now carries an @deprecated JSDoc annotation.

What the implementation actually says. In packages/server/src/server/server.ts the following are all marked @deprecated with text pointing at the ctx.mcpReq.* / ctx.clientCapabilities equivalents:

  • getClientCapabilities() — line 797
  • createMessage() (all three overloads) — lines 830, 838, 846
  • elicitInput() — line 874
  • listRoots() — line 930
  • sendLoggingMessage() — line 942

Each annotation reads roughly: @deprecated Use ctx.mcpReq.X inside a handler. Works under both protocols. This top-level form requires a pre-2026 connection. These were added in the same v2-stateless stack (#2131).

Step-by-step proof of the surprise. A maintainer of an existing pre-2026 server reads the new migration section, sees "this migration is recommended but not required" / "They are not removed," and reasonably concludes the existing server.elicitInput(...) / server.createMessage(...) calls in their handlers can stay as-is for now. They upgrade the SDK. Their editor immediately renders strikethrough on every one of those call sites, and any eslint-plugin-deprecation / @typescript-eslint/no-deprecated-style lint rule starts failing. The migration doc — whose entire purpose is to set expectations about this upgrade — said nothing about that, even though the deprecation is the single most visible signal a developer will receive about the change.

Why this matters. "Not removed" and "not deprecated" are different claims. The current prose is technically accurate but communicates a lower urgency than the codebase signals. This is exactly the class of "prose that doesn't match the implementation in the same diff stack" that the repo's own CLAUDE.md Breaking Changes guidance is trying to prevent.

How to fix. A one-clause addition to each passage closes the gap:

  • docs/migration.md (~line 897): change "They are not removed." → "They are not removed but are now marked @deprecated; expect IDE/lint warnings on them."
  • docs/migration-SKILL.md §15: change "…this migration is recommended but not required." → "…this migration is recommended but not required; the top-level methods are now @deprecated and will produce IDE/lint warnings."

Documentation-completeness nit: no runtime impact, but the omission directly contradicts the reader's first-hand IDE experience after upgrading.


MRTR via `InputRequiredError` works for handlers registered via `setRequestHandler`; `fallbackRequestHandler` is not wrapped by middleware (matches pre-existing behavior).

### Tests pinning pre-2026

If a test exercises pre-2026 connection-model behavior (e.g., `oninitialized`, server-initiated requests, in-band logging) and breaks because the auto-probe now succeeds, construct the client with `supportedProtocolVersions` filtered to pre-2026 versions only, or use a fixture like `LegacyTestClient` that does so. The probe falls back to legacy when no mutual stateless version exists.

Check warning on line 904 in docs/migration.md

View check run for this annotation

Claude / Claude Code Review

migration.md references internal-only test fixture LegacyTestClient

The "Tests pinning pre-2026" subsection references `LegacyTestClient` as if it were an importable helper, but it's an internal test fixture (`test/integration/test/__fixtures__/testClient.ts`) not exported by any `@modelcontextprotocol/*` package. Since `migration.md` is the public end-user migration guide, consider dropping the mention or qualifying it as an internal-repo pattern (the parallel sentence in `migration-SKILL.md` §15 already omits it).
Comment on lines +902 to +904
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 The "Tests pinning pre-2026" subsection references LegacyTestClient as if it were an importable helper, but it's an internal test fixture (test/integration/test/__fixtures__/testClient.ts) not exported by any @modelcontextprotocol/* package. Since migration.md is the public end-user migration guide, consider dropping the mention or qualifying it as an internal-repo pattern (the parallel sentence in migration-SKILL.md §15 already omits it).

Extended reasoning...

What the issue is

The new "Tests pinning pre-2026" subsection in docs/migration.md says:

...construct the client with supportedProtocolVersions filtered to pre-2026 versions only, or use a fixture like LegacyTestClient that does so.

LegacyTestClient is defined only in test/integration/test/__fixtures__/testClient.ts inside this repository. It is not exported from @modelcontextprotocol/core, /client, /server, or any other published package — a grep across all packages/* index files turns up nothing.

Why it matters

docs/migration.md is the public, end-user migration guide. An SDK consumer reading this sentence has no way to know that LegacyTestClient is an internal test fixture rather than an exported helper. The natural next step is to grep their node_modules (or the package docs) for LegacyTestClient, find nothing, and waste time before realizing the symbol only exists inside this repo's test tree.

Why the parallel doc gets it right

The same guidance appears in docs/migration-SKILL.md §15:

For tests that exercise pre-2026 connection-model behavior, construct the test client with supportedProtocolVersions filtered to pre-2026 versions only.

That copy deliberately omits the LegacyTestClient reference and just describes the actionable pattern. The two docs are otherwise meant to be parallel (per CLAUDE.md, breaking-change guidance should be documented in both), so the divergence reads like an oversight rather than a conscious choice.

Step-by-step proof

  1. Open docs/migration.md → "Tests pinning pre-2026" → see the LegacyTestClient reference.
  2. Run grep -r LegacyTestClient packages/ → zero matches.
  3. Run grep -r LegacyTestClient . → matches only under test/integration/test/__fixtures__/testClient.ts and integration test files.
  4. Inspect any packages/*/src/index.ts → no LegacyTestClient export.
  5. Conclusion: the referenced symbol cannot be imported by a consumer following the guide.

How to fix

Either drop the reference entirely (matching the SKILL.md wording), or rephrase so it's unambiguous that it's a pattern to copy, not a symbol to import — e.g.:

...construct the client with supportedProtocolVersions filtered to pre-2026 versions only (see this repo's internal LegacyTestClient test fixture for an example of that pattern).

This is a documentation-clarity nit only: the primary actionable instruction (supportedProtocolVersions) is still present and correct, and the word "like" softens the implication slightly. But naming an unexported symbol in a public guide invites a fruitless search.


### Shared instances

A single `Server` instance can safely serve many concurrent 2026-06 clients via `handleHttp` or a connected transport's per-message router. For pre-2026 clients, the existing per-instance isolation guidance still applies (the legacy path's `_clientCapabilities` is per-connection state): either per-session (a transport map keyed by session ID) or a fresh server per request.

### Dual-mode endpoint

To serve both protocol eras from one HTTP endpoint, use `WebStandardStreamableHTTPServerTransport`'s per-message router for both eras, or compose `handleHttp` with a legacy transport behind your own router (e.g. branch on `MCP-Protocol-Version` / `isInitializeRequest` to send pre-2026 traffic to a per-session transport and everything else to the shared `handleHttp` handler). The shared `Server` instance handles 2026-06 traffic; pre-2026 traffic gets per-instance isolation as above.

Check warning on line 912 in docs/migration.md

View check run for this annotation

Claude / Claude Code Review

Dual-mode endpoint first option contradicts Shared instances guidance

The first option in the new "Dual-mode endpoint" subsection ("use `WebStandardStreamableHTTPServerTransport`'s per-message router for both eras") reads as a complete dual-mode recipe, but the per-message router on a single shared transport routes pre-2026 traffic to the same connected `Server` instance — exactly what the immediately-preceding "Shared instances" subsection (and the `NOTE` added to `simpleStatelessStreamableHttp.ts` in this PR) say is unsafe for concurrent pre-2026 clients. Sugges
Comment on lines +910 to +912
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 The first option in the new "Dual-mode endpoint" subsection ("use WebStandardStreamableHTTPServerTransport's per-message router for both eras") reads as a complete dual-mode recipe, but the per-message router on a single shared transport routes pre-2026 traffic to the same connected Server instance — exactly what the immediately-preceding "Shared instances" subsection (and the NOTE added to simpleStatelessStreamableHttp.ts in this PR) say is unsafe for concurrent pre-2026 clients. Suggest qualifying option 1 (e.g., "the per-message router on a per-session transport map") or merging it into option 2 so the closing "pre-2026 traffic gets per-instance isolation as above" sentence is actually satisfied by both options.

Extended reasoning...

The contradiction

The "Dual-mode endpoint" subsection added in commit 4e002f6 says:

To serve both protocol eras from one HTTP endpoint, use WebStandardStreamableHTTPServerTransport's per-message router for both eras, or compose handleHttp with a legacy transport behind your own router [...]. The shared Server instance handles 2026-06 traffic; pre-2026 traffic gets per-instance isolation as above.

The closing sentence asserts pre-2026 traffic gets per-instance isolation. That holds for option 2 (your own router branches pre-2026 traffic to a per-session transport map). It does not hold for option 1 as written, and the immediately-preceding "Shared instances" subsection — added in the same diff — already explains why.

What the per-message router actually does

WebStandardStreamableHTTPServerTransport.handleRequest() (packages/server/src/server/streamableHttp.ts ~lines 380–403) routes 2026-06 requests to statelessHttpHandler and falls through to the legacy stateful path for pre-2026 requests. The legacy path dispatches into the single connected Server, which holds _clientCapabilities as per-instance state (set during initialize, packages/server/src/server/server.ts). So one shared transport+Server using only "the per-message router for both eras" means concurrent pre-2026 clients clobber each other's _clientCapabilities.

Why this is internally inconsistent within the PR

The "Shared instances" subsection two paragraphs earlier says exactly that:

For pre-2026 clients, the existing per-instance isolation guidance still applies (the legacy path's _clientCapabilities is per-connection state): either per-session (a transport map keyed by session ID) or a fresh server per request.

And examples/server/src/simpleStatelessStreamableHttp.ts (rewritten in this PR to demonstrate the shared-server + connected-transport architecture) carries the matching warning: "NOTE: a single shared instance is NOT safe for concurrent pre-2026 clients." The Dual-mode subsection's option 1 describes that same architecture as a viable both-eras recipe.

Step-by-step reader walkthrough

  1. A reader who needs to serve both protocol eras from one endpoint reads the "Shared instances" subsection and learns: shared instance OK for 2026-06, pre-2026 needs per-session or fresh-server-per-request isolation.
  2. They scroll to "Dual-mode endpoint" and read option 1: "use WebStandardStreamableHTTPServerTransport's per-message router for both eras."
  3. They construct one shared Server, connect one WebStandardStreamableHTTPServerTransport, mount transport.handleRequest() — the architecture from simpleStatelessStreamableHttp.ts minus the warning.
  4. The closing sentence ("pre-2026 traffic gets per-instance isolation as above") tells them they're done. But nothing in option 1 spawns a fresh Server or routes pre-2026 traffic to a per-session map; the per-message router does not provide isolation by itself. The reader has built the configuration the "Shared instances" subsection just warned against.

Why this is a nit, not a blocking bug

This is a docs-only consistency issue in newly-added prose, with no runtime impact. Option 2 is a complete recipe and the closing sentence partially mitigates the confusion by signaling that pre-2026 isolation is required. But option 1 as worded reads as if the per-message router alone is sufficient, contradicting the example comment and the preceding subsection.

Suggested fix

Either drop option 1, qualify it (e.g., "use WebStandardStreamableHTTPServerTransport's per-message router on a per-session transport map for pre-2026 traffic, with a shared Server for 2026-06"), or merge the two options into one sentence describing the single composition: a router that dispatches pre-2026 traffic to a per-session transport and everything else to the shared handleHttp/per-message-router handler.


## Enhancements

### Automatic JSON Schema validator selection by runtime
Expand Down
8 changes: 4 additions & 4 deletions docs/server.md
Original file line number Diff line number Diff line change
Expand Up @@ -478,7 +478,7 @@ For runnable examples, see [`elicitationFormExample.ts`](https://github.com/mode

### Roots

Roots let a tool handler discover the client's workspace directories — for example, to scope a file search or identify project boundaries (see [Roots](https://modelcontextprotocol.io/docs/learn/client-concepts#roots) in the MCP overview). Call {@linkcode @modelcontextprotocol/server!server/server.Server#listRoots | server.server.listRoots()} (requires the client to declare the `roots` capability):
Roots let a tool handler discover the client's workspace directories — for example, to scope a file search or identify project boundaries (see [Roots](https://modelcontextprotocol.io/docs/learn/client-concepts#roots) in the MCP overview). Call `ctx.mcpReq.listRoots()` inside the handler (requires the client to declare the `roots` capability). This works under both the pre-2026 connection model and the 2026 stateless model:

```ts source="../examples/server/src/serverGuide.examples.ts#registerTool_roots"
server.registerTool(
Comment thread
claude[bot] marked this conversation as resolved.
Expand All @@ -487,8 +487,8 @@ server.registerTool(
description: 'List files across all workspace roots',
inputSchema: z.object({})
},
async (_args, _ctx): Promise<CallToolResult> => {
const { roots } = await server.server.listRoots();
async (_args, ctx): Promise<CallToolResult> => {
const { roots } = await ctx.mcpReq.listRoots();
const summary = roots.map(r => `${r.name ?? r.uri}: ${r.uri}`).join('\n');
return { content: [{ type: 'text', text: summary }] };
}
Expand Down Expand Up @@ -572,7 +572,7 @@ If you use `NodeStreamableHTTPServerTransport` directly with your own HTTP frame

| Feature | Description | Example |
|---------|-------------|---------|
| Web Standard transport | Deploy on Cloudflare Workers, Deno, or Bun | [`honoWebStandardStreamableHttp.ts`](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/examples/server/src/honoWebStandardStreamableHttp.ts) |
| 2026-06 stateless `handleHttp()` (Hono) | One shared server, no Transport, no `connect()` — runs on Cloudflare Workers, Deno, or Bun | [`honoWebStandardStreamableHttp.ts`](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/examples/server/src/honoWebStandardStreamableHttp.ts) |
| Session management | Per-session transport routing, initialization, and cleanup | [`simpleStreamableHttp.ts`](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/examples/server/src/simpleStreamableHttp.ts) |
| Resumability | Replay missed SSE events via an event store | [`inMemoryEventStore.ts`](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/examples/server/src/inMemoryEventStore.ts) |
| CORS | Expose MCP headers for browser clients | [`simpleStreamableHttp.ts`](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/examples/server/src/simpleStreamableHttp.ts) |
Expand Down
Loading
Loading