Skip to content

Commit c73c457

Browse files
docs: examples + 2026-06 migration guide + changeset
Examples migrated to ctx.mcpReq.* (works under both protocols): - toolWithSampleServer.ts: server.createMessage -> ctx.mcpReq.requestSampling - elicitationFormExample.ts: server.elicitInput -> ctx.mcpReq.elicitInput (3x) - serverGuide.examples.ts: server.listRoots -> ctx.mcpReq.listRoots - elicitationUrlExample.ts: out-of-band webhook keeps server.elicitInput; comment documents pre-2026-only limitation (no request context). Headline 2026-06 example: - honoWebStandardStreamableHttp.ts: handleHttp(server.server, opts) Fetch handler. One shared instance, no Transport, no connect(). - simpleStatelessStreamableHttp.ts: shared instance + single connected transport (was per-request); transport per-message router serves both. docs/migration.md + docs/migration-SKILL.md: 2026-06 stateless support section. Server/Client are the same classes (additive); ctx.mcpReq.* mapping table; auto-probe note; subscribe() note; LegacyTestClient pattern for tests. No .legacy/LegacyServer references (sectioned approach). docs/server.md, examples/server/README.md: updated to match. .changeset/stateless-2026-06.md: major bump core/server/client. Satisfies: 2575-R1, 2567-R1, 2322-R1
1 parent 32ef72b commit c73c457

12 files changed

Lines changed: 151 additions & 49 deletions

.changeset/stateless-2026-06.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
---
2+
'@modelcontextprotocol/core': major
3+
'@modelcontextprotocol/server': major
4+
'@modelcontextprotocol/client': major
5+
---
6+
7+
2026-06 stateless protocol support (SEP-2575, SEP-2567, SEP-2322).
8+
9+
`Server` and `Client` now support the 2026-06 stateless connection model
10+
alongside the existing pre-2026 model. They remain the same classes (still
11+
extending `Protocol`); the new behavior is additive.
12+
13+
- `Client.connect()` auto-probes `server/discover` and falls back to the
14+
legacy `initialize` handshake.
15+
- `Server` gained `subscriptions` and `statelessHandlers()`; transports route
16+
per-message via the `MCP-Protocol-Version` header / `_meta` key.
17+
- `handleHttp(server, opts)` is a new Fetch-API entry point: one shared
18+
`Server` instance, no `Transport`, no `connect()`.
19+
- `client.subscribe(filter)` opens a `subscriptions/listen` stream.
20+
- `Transport` interface gained optional `setStatelessHandlers?` and
21+
`sendAndReceive?` for custom transports.
22+
- Prefer `ctx.mcpReq.{elicitInput, requestSampling, listRoots, log}` inside
23+
handlers; works under both protocols (MRTR under 2026-06).
24+
25+
See `docs/migration.md` for the full guide.

CLAUDE.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,6 @@ When a request arrives from the remote side:
161161
1. **Transport** receives message, calls `transport.onmessage()`
162162
2. **`Protocol.connect()`** routes to `_onrequest()`, `_onresponse()`, or `_onnotification()`
163163
3. **`Protocol._onrequest()`**:
164-
- Looks up handler in `_requestHandlers` map (keyed by method name)
165164
- Creates `BaseContext` with `signal`, `sessionId`, `sendNotification`, `sendRequest`, etc.
166165
- Calls `buildContext()` to let subclasses enrich the context (e.g., Server adds HTTP request info)
167166
- Calls `dispatcher.dispatch()` which looks up the handler (keyed by method name), runs the middleware chain, invokes the handler, and wraps the result as a JSON-RPC response

docs/migration-SKILL.md

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -524,7 +524,31 @@ Access validators explicitly:
524524
- AJV (Node.js): `import { AjvJsonSchemaValidator } from '@modelcontextprotocol/server';`
525525
- CF Worker: `import { CfWorkerJsonSchemaValidator } from '@modelcontextprotocol/server/validators/cf-worker';`
526526

527-
## 15. Migration Steps (apply in this order)
527+
## 15. 2026-06 Stateless Support (SEP-2575/2567/2322)
528+
529+
`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.
530+
531+
**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:
532+
533+
| Find calls to | Replace with |
534+
| --- | --- |
535+
| `.createMessage(` | `ctx.mcpReq.requestSampling(` |
536+
| `.elicitInput(` | `ctx.mcpReq.elicitInput(` |
537+
| `.listRoots(` | `ctx.mcpReq.listRoots(` |
538+
| `.sendLoggingMessage({ level, data })` | `ctx.mcpReq.log(level, data)` |
539+
| `.getClientCapabilities()` | `ctx.clientCapabilities` |
540+
541+
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).
542+
543+
The top-level `server.createMessage()` etc. still work with a connected pre-2026 client; this migration is recommended but not required.
544+
545+
`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.
546+
547+
MRTR via `InputRequiredError` works for handlers registered via `setRequestHandler`; `fallbackRequestHandler` is not wrapped by middleware (matches pre-existing behavior).
548+
549+
For tests that exercise pre-2026 connection-model behavior, construct the test client with `supportedProtocolVersions` filtered to pre-2026 versions only.
550+
551+
## 16. Migration Steps (apply in this order)
528552

529553
1. Update `package.json`: `npm uninstall @modelcontextprotocol/sdk`, install the appropriate v2 packages
530554
2. Replace all imports from `@modelcontextprotocol/sdk/...` using the import mapping tables (sections 3-4), including `StreamableHTTPServerTransport``NodeStreamableHTTPServerTransport`
@@ -536,4 +560,5 @@ Access validators explicitly:
536560
8. If using server SSE transport, migrate to Streamable HTTP
537561
9. If using server auth from the SDK: RS helpers (`requireBearerAuth`, `mcpAuthMetadataRouter`) → `@modelcontextprotocol/express`; AS helpers → external IdP/OAuth library
538562
10. If relying on `listTools()`/`listPrompts()`/etc. throwing on missing capabilities, set `enforceStrictCapabilities: true`
539-
11. Verify: build with `tsc` / run tests
563+
11. Inside tool/prompt/resource handlers, replace `server.createMessage`/`elicitInput`/`listRoots`/`sendLoggingMessage` with `ctx.mcpReq.*` per section 15
564+
12. Verify: build with `tsc` / run tests

docs/migration.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -874,6 +874,42 @@ There is no migration path for the removed surface; it was always `@experimental
874874

875875
`TaskCreationParams.ttl` (the storage-layer creation parameter) is now `number | undefined`; `null` is no longer accepted. Per the MCP spec, `null` TTL (unlimited lifetime) is only valid in server responses (`Task.ttl`), not in creation requests. Omit `ttl` to let the store decide. This is a storage-interface change and is independent of the Protocol-level removals above.
876876

877+
## 2026-06 Stateless Protocol Support (SEP-2575, SEP-2567, SEP-2322)
878+
879+
`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.
880+
881+
### What changed
882+
883+
- **`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.
884+
- **`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.
885+
- **`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`.
886+
- **`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`).
887+
- **`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.
888+
889+
### Prefer `ctx.mcpReq.*` for server-to-client interactions
890+
891+
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).
892+
893+
| Top-level (pre-2026 only) | Handler-context (both protocols) |
894+
| --- | --- |
895+
| `server.createMessage(params)` | `ctx.mcpReq.requestSampling(params)` |
896+
| `server.elicitInput(params)` | `ctx.mcpReq.elicitInput(params)` |
897+
| `server.listRoots()` | `ctx.mcpReq.listRoots()` |
898+
| `server.sendLoggingMessage({level, data})` | `ctx.mcpReq.log(level, data)` |
899+
| `server.getClientCapabilities()` | `ctx.clientCapabilities` |
900+
901+
The top-level methods still exist and work when a pre-2026 client is connected. They are not removed.
902+
903+
MRTR via `InputRequiredError` works for handlers registered via `setRequestHandler`; `fallbackRequestHandler` is not wrapped by middleware (matches pre-existing behavior).
904+
905+
### Tests pinning pre-2026
906+
907+
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.
908+
909+
### Shared instances
910+
911+
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-session guidance still applies (the legacy path's `_clientCapabilities` is per-connection state).
912+
877913
## Enhancements
878914

879915
### Automatic JSON Schema validator selection by runtime

docs/server.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -478,7 +478,7 @@ For runnable examples, see [`elicitationFormExample.ts`](https://github.com/mode
478478

479479
### Roots
480480

481-
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):
481+
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:
482482

483483
```ts source="../examples/server/src/serverGuide.examples.ts#registerTool_roots"
484484
server.registerTool(
@@ -487,8 +487,8 @@ server.registerTool(
487487
description: 'List files across all workspace roots',
488488
inputSchema: z.object({})
489489
},
490-
async (_args, _ctx): Promise<CallToolResult> => {
491-
const { roots } = await server.server.listRoots();
490+
async (_args, ctx): Promise<CallToolResult> => {
491+
const { roots } = await ctx.mcpReq.listRoots();
492492
const summary = roots.map(r => `${r.name ?? r.uri}: ${r.uri}`).join('\n');
493493
return { content: [{ type: 'text', text: summary }] };
494494
}

examples/server/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,15 +28,15 @@ pnpm tsx src/simpleStreamableHttp.ts
2828
| Scenario | Description | File |
2929
| ----------------------------------------- | -------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- |
3030
| Streamable HTTP server (stateful) | Feature-rich server with tools/resources/prompts, logging, sampling, and optional OAuth. | [`src/simpleStreamableHttp.ts`](src/simpleStreamableHttp.ts) |
31-
| Streamable HTTP server (stateless) | No session tracking; good for simple API-style servers. | [`src/simpleStatelessStreamableHttp.ts`](src/simpleStatelessStreamableHttp.ts) |
31+
| Streamable HTTP server (stateless) | Shared instance for 2026-06 clients. Pre-2026 clients need per-session (see stateful row). | [`src/simpleStatelessStreamableHttp.ts`](src/simpleStatelessStreamableHttp.ts) |
3232
| Resource-Server-only auth | Minimal OAuth RS using SDK's `mcpAuthMetadataRouter` + `requireBearerAuth` (no better-auth). | [`src/resourceServerOnly.ts`](src/resourceServerOnly.ts) |
3333
| JSON response mode (no SSE) | Streamable HTTP with JSON-only responses and limited notifications. | [`src/jsonResponseStreamableHttp.ts`](src/jsonResponseStreamableHttp.ts) |
3434
| Server notifications over Streamable HTTP | Demonstrates server-initiated notifications via GET+SSE. | [`src/standaloneSseWithGetStreamableHttp.ts`](src/standaloneSseWithGetStreamableHttp.ts) |
3535
| Output schema server | Demonstrates tool output validation with structured output schemas. | [`src/mcpServerOutputSchema.ts`](src/mcpServerOutputSchema.ts) |
3636
| Form elicitation server | Collects **non-sensitive** user input via schema-driven forms. | [`src/elicitationFormExample.ts`](src/elicitationFormExample.ts) |
3737
| URL elicitation server | Secure browser-based flows for **sensitive** input (API keys, OAuth, payments). | [`src/elicitationUrlExample.ts`](src/elicitationUrlExample.ts) |
3838
| Sampling server | Demonstrates server-initiated sampling requests. | [`src/toolWithSampleServer.ts`](src/toolWithSampleServer.ts) |
39-
| Hono Streamable HTTP server | Streamable HTTP server built with Hono instead of Express. | [`src/honoWebStandardStreamableHttp.ts`](src/honoWebStandardStreamableHttp.ts) |
39+
| Hono `handleHttp` server (2026-06) | Headline 2026-06 stateless entry: `handleHttp()` Fetch handler, no Transport, no connect(). | [`src/honoWebStandardStreamableHttp.ts`](src/honoWebStandardStreamableHttp.ts) |
4040
| SSE polling demo server | Legacy SSE server intended for polling demos. | [`src/ssePollingExample.ts`](src/ssePollingExample.ts) |
4141

4242
## OAuth demo flags (Streamable HTTP server)

examples/server/src/elicitationFormExample.ts

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,12 @@ const getServer = () => {
3636
{
3737
description: 'Register a new user account by collecting their information'
3838
},
39-
async () => {
39+
async ctx => {
4040
try {
41-
// Request user information through form elicitation
42-
const result = await mcpServer.server.elicitInput({
41+
// Request user information through form elicitation.
42+
// ctx.mcpReq.elicitInput works under both the pre-2026 connection
43+
// model and the 2026 stateless model (MRTR). See SEP-2322.
44+
const result = await ctx.mcpReq.elicitInput({
4345
mode: 'form',
4446
message: 'Please provide your registration information:',
4547
requestedSchema: {
@@ -134,10 +136,10 @@ const getServer = () => {
134136
{
135137
description: 'Create a calendar event by collecting event details'
136138
},
137-
async () => {
139+
async ctx => {
138140
try {
139141
// Step 1: Collect basic event information
140-
const basicInfo = await mcpServer.server.elicitInput({
142+
const basicInfo = await ctx.mcpReq.elicitInput({
141143
mode: 'form',
142144
message: 'Step 1: Enter basic event information',
143145
requestedSchema: {
@@ -166,7 +168,7 @@ const getServer = () => {
166168
}
167169

168170
// Step 2: Collect date and time
169-
const dateTime = await mcpServer.server.elicitInput({
171+
const dateTime = await ctx.mcpReq.elicitInput({
170172
mode: 'form',
171173
message: 'Step 2: Enter date and time',
172174
requestedSchema: {
@@ -238,9 +240,9 @@ const getServer = () => {
238240
{
239241
description: 'Update shipping address with validation'
240242
},
241-
async () => {
243+
async ctx => {
242244
try {
243-
const result = await mcpServer.server.elicitInput({
245+
const result = await ctx.mcpReq.elicitInput({
244246
mode: 'form',
245247
message: 'Please provide your shipping address:',
246248
requestedSchema: {

examples/server/src/elicitationUrlExample.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -577,6 +577,11 @@ const mcpPostHandler = async (req: Request, res: Response) => {
577577
// This avoids race conditions where requests might come in before the session is stored
578578
console.log(`Session initialized with ID: ${sessionId}`);
579579
transports[sessionId] = transport;
580+
// Out-of-band webhook callbacks have no request context, so this
581+
// calls server.elicitInput directly. This pattern requires a
582+
// connected pre-2026 client; under the 2026 stateless model
583+
// there is no session to send to. Prefer ctx.mcpReq.elicitInput
584+
// inside a tool/prompt handler when possible.
580585
sessionsNeedingElicitation[sessionId] = {
581586
elicitationSender: params => server.server.elicitInput(params),
582587
createCompletionNotifier: elicitationId => server.server.createElicitationCompletionNotifier(elicitationId)

0 commit comments

Comments
 (0)