diff --git a/CLAUDE.md b/CLAUDE.md index ac88c3fe2..2f7df4f85 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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`. diff --git a/docs/migration-SKILL.md b/docs/migration-SKILL.md index ee19c1cea..01e6c3d6c 100644 --- a/docs/migration-SKILL.md +++ b/docs/migration-SKILL.md @@ -429,6 +429,9 @@ Request/notification params remain fully typed. Remove unused schema imports aft | `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 | + +`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) @@ -522,7 +525,33 @@ Access validators explicitly: - 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 `.server.X()` (or `server.X()` on a low-level `Server`) with `ctx.mcpReq.X()`. The `` 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(` | +| `.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` @@ -534,4 +563,5 @@ Access validators explicitly: 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 diff --git a/docs/migration.md b/docs/migration.md index 1c2311a61..07b09cdc4 100644 --- a/docs/migration.md +++ b/docs/migration.md @@ -645,7 +645,7 @@ server.setRequestHandler('tools/call', async (request, ctx) => { }); ``` -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 @@ -869,6 +869,48 @@ The 2025-11 experimental tasks side-channel woven through `Protocol` has been re 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`. 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). + +| 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. + +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. + +### 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. + ## Enhancements ### Automatic JSON Schema validator selection by runtime diff --git a/docs/server.md b/docs/server.md index b16c24fc4..6fe8cede9 100644 --- a/docs/server.md +++ b/docs/server.md @@ -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( @@ -487,8 +487,8 @@ server.registerTool( description: 'List files across all workspace roots', inputSchema: z.object({}) }, - async (_args, _ctx): Promise => { - const { roots } = await server.server.listRoots(); + async (_args, ctx): Promise => { + 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 }] }; } @@ -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) | diff --git a/examples/server/README.md b/examples/server/README.md index 0d7821547..d0c65c417 100644 --- a/examples/server/README.md +++ b/examples/server/README.md @@ -25,19 +25,19 @@ pnpm tsx src/simpleStreamableHttp.ts ## Example index -| Scenario | Description | File | -| ----------------------------------------- | -------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | -| Streamable HTTP server (stateful) | Feature-rich server with tools/resources/prompts, logging, sampling, and optional OAuth. | [`src/simpleStreamableHttp.ts`](src/simpleStreamableHttp.ts) | -| Streamable HTTP server (stateless) | No session tracking; good for simple API-style servers. | [`src/simpleStatelessStreamableHttp.ts`](src/simpleStatelessStreamableHttp.ts) | -| Resource-Server-only auth | Minimal OAuth RS using SDK's `mcpAuthMetadataRouter` + `requireBearerAuth` (no better-auth). | [`src/resourceServerOnly.ts`](src/resourceServerOnly.ts) | -| JSON response mode (no SSE) | Streamable HTTP with JSON-only responses and limited notifications. | [`src/jsonResponseStreamableHttp.ts`](src/jsonResponseStreamableHttp.ts) | -| Server notifications over Streamable HTTP | Demonstrates server-initiated notifications via GET+SSE. | [`src/standaloneSseWithGetStreamableHttp.ts`](src/standaloneSseWithGetStreamableHttp.ts) | -| Output schema server | Demonstrates tool output validation with structured output schemas. | [`src/mcpServerOutputSchema.ts`](src/mcpServerOutputSchema.ts) | -| Form elicitation server | Collects **non-sensitive** user input via schema-driven forms. | [`src/elicitationFormExample.ts`](src/elicitationFormExample.ts) | -| URL elicitation server | Secure browser-based flows for **sensitive** input (API keys, OAuth, payments). | [`src/elicitationUrlExample.ts`](src/elicitationUrlExample.ts) | -| Sampling server | Demonstrates server-initiated sampling requests. | [`src/toolWithSampleServer.ts`](src/toolWithSampleServer.ts) | -| Hono Streamable HTTP server | Streamable HTTP server built with Hono instead of Express. | [`src/honoWebStandardStreamableHttp.ts`](src/honoWebStandardStreamableHttp.ts) | -| SSE polling demo server | Legacy SSE server intended for polling demos. | [`src/ssePollingExample.ts`](src/ssePollingExample.ts) | +| Scenario | Description | File | +| ----------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------- | +| Streamable HTTP server (stateful) | Feature-rich server with tools/resources/prompts, logging, sampling, and optional OAuth. | [`src/simpleStreamableHttp.ts`](src/simpleStreamableHttp.ts) | +| Streamable HTTP server (stateless) | Shared instance for 2026-06 clients. Pre-2026 clients need per-instance isolation: per-session (see stateful row) or a fresh server per request. | [`src/simpleStatelessStreamableHttp.ts`](src/simpleStatelessStreamableHttp.ts) | +| Resource-Server-only auth | Minimal OAuth RS using SDK's `mcpAuthMetadataRouter` + `requireBearerAuth` (no better-auth). | [`src/resourceServerOnly.ts`](src/resourceServerOnly.ts) | +| JSON response mode (no SSE) | Streamable HTTP with JSON-only responses and limited notifications. | [`src/jsonResponseStreamableHttp.ts`](src/jsonResponseStreamableHttp.ts) | +| Server notifications over Streamable HTTP | Demonstrates server-initiated notifications via GET+SSE. | [`src/standaloneSseWithGetStreamableHttp.ts`](src/standaloneSseWithGetStreamableHttp.ts) | +| Output schema server | Demonstrates tool output validation with structured output schemas. | [`src/mcpServerOutputSchema.ts`](src/mcpServerOutputSchema.ts) | +| Form elicitation server | Collects **non-sensitive** user input via schema-driven forms. | [`src/elicitationFormExample.ts`](src/elicitationFormExample.ts) | +| URL elicitation server | Secure browser-based flows for **sensitive** input (API keys, OAuth, payments). | [`src/elicitationUrlExample.ts`](src/elicitationUrlExample.ts) | +| Sampling server | Demonstrates server-initiated sampling requests. | [`src/toolWithSampleServer.ts`](src/toolWithSampleServer.ts) | +| Hono `handleHttp` server (2026-06) | Headline 2026-06 stateless entry: `handleHttp()` Fetch handler, no Transport, no connect(). | [`src/honoWebStandardStreamableHttp.ts`](src/honoWebStandardStreamableHttp.ts) | +| SSE polling demo server | Legacy SSE server intended for polling demos. | [`src/ssePollingExample.ts`](src/ssePollingExample.ts) | ## OAuth demo flags (Streamable HTTP server) diff --git a/examples/server/src/elicitationFormExample.ts b/examples/server/src/elicitationFormExample.ts index e059e8452..7c661dcb1 100644 --- a/examples/server/src/elicitationFormExample.ts +++ b/examples/server/src/elicitationFormExample.ts @@ -36,10 +36,12 @@ const getServer = () => { { description: 'Register a new user account by collecting their information' }, - async () => { + async ctx => { try { - // Request user information through form elicitation - const result = await mcpServer.server.elicitInput({ + // Request user information through form elicitation. + // ctx.mcpReq.elicitInput works under both the pre-2026 connection + // model and the 2026 stateless model (MRTR). See SEP-2322. + const result = await ctx.mcpReq.elicitInput({ mode: 'form', message: 'Please provide your registration information:', requestedSchema: { @@ -134,10 +136,10 @@ const getServer = () => { { description: 'Create a calendar event by collecting event details' }, - async () => { + async ctx => { try { // Step 1: Collect basic event information - const basicInfo = await mcpServer.server.elicitInput({ + const basicInfo = await ctx.mcpReq.elicitInput({ mode: 'form', message: 'Step 1: Enter basic event information', requestedSchema: { @@ -166,7 +168,7 @@ const getServer = () => { } // Step 2: Collect date and time - const dateTime = await mcpServer.server.elicitInput({ + const dateTime = await ctx.mcpReq.elicitInput({ mode: 'form', message: 'Step 2: Enter date and time', requestedSchema: { @@ -238,9 +240,9 @@ const getServer = () => { { description: 'Update shipping address with validation' }, - async () => { + async ctx => { try { - const result = await mcpServer.server.elicitInput({ + const result = await ctx.mcpReq.elicitInput({ mode: 'form', message: 'Please provide your shipping address:', requestedSchema: { diff --git a/examples/server/src/elicitationUrlExample.ts b/examples/server/src/elicitationUrlExample.ts index 93b59152f..e56fe1b6e 100644 --- a/examples/server/src/elicitationUrlExample.ts +++ b/examples/server/src/elicitationUrlExample.ts @@ -577,6 +577,11 @@ const mcpPostHandler = async (req: Request, res: Response) => { // This avoids race conditions where requests might come in before the session is stored console.log(`Session initialized with ID: ${sessionId}`); transports[sessionId] = transport; + // Out-of-band webhook callbacks have no request context, so this + // calls server.elicitInput directly. This pattern requires a + // connected pre-2026 client; under the 2026 stateless model + // there is no session to send to. Prefer ctx.mcpReq.elicitInput + // inside a tool/prompt handler when possible. sessionsNeedingElicitation[sessionId] = { elicitationSender: params => server.server.elicitInput(params), createCompletionNotifier: elicitationId => server.server.createElicitationCompletionNotifier(elicitationId) diff --git a/examples/server/src/honoWebStandardStreamableHttp.ts b/examples/server/src/honoWebStandardStreamableHttp.ts index b15f9885f..c4cb038ff 100644 --- a/examples/server/src/honoWebStandardStreamableHttp.ts +++ b/examples/server/src/honoWebStandardStreamableHttp.ts @@ -1,20 +1,27 @@ /** - * Example MCP server using Hono with WebStandardStreamableHTTPServerTransport + * Example MCP server using Hono with the 2026-06 stateless entry point. * - * This example demonstrates using the Web Standard transport directly with Hono, - * which works on any runtime: Node.js, Cloudflare Workers, Deno, Bun, etc. + * This is the headline 2026-06 (SEP-2575) example: one shared McpServer + * instance, no Transport object, no connect(). handleHttp() returns a + * Fetch-API (Request) => Response handler that any web-standard runtime + * can mount: Node.js, Cloudflare Workers, Deno, Bun, etc. + * + * Pre-2026 clients are not served by this entry. For a dual-mode setup + * (one endpoint serving both protocols), see "Dual-mode endpoint" in the + * 2026-06 section of `docs/migration.md`. * * Run with: pnpm tsx src/honoWebStandardStreamableHttp.ts */ import { serve } from '@hono/node-server'; import type { CallToolResult } from '@modelcontextprotocol/server'; -import { McpServer, WebStandardStreamableHTTPServerTransport } from '@modelcontextprotocol/server'; +import { handleHttp, McpServer } from '@modelcontextprotocol/server'; import { Hono } from 'hono'; import { cors } from 'hono/cors'; import * as z from 'zod/v4'; -// Create the MCP server +// Create one shared MCP server instance. Under the 2026-06 stateless model +// there is no per-session state, so a single instance handles all requests. const server = new McpServer({ name: 'hono-webstandard-mcp-server', version: '1.0.0' @@ -30,13 +37,16 @@ server.registerTool( }, async ({ name }): Promise => { return { - content: [{ type: 'text', text: `Hello, ${name}! (from Hono + WebStandard transport)` }] + content: [{ type: 'text', text: `Hello, ${name}! (from Hono + handleHttp)` }] }; } ); -// Create a stateless transport (no options = no session management) -const transport = new WebStandardStreamableHTTPServerTransport(); +// handleHttp(server, opts) returns a (Request) => Promise handler. +// No Transport, no connect(); each HTTP request is dispatched independently. +const mcpHandler = handleHttp(server.server, { + allowedHosts: ['localhost', '127.0.0.1'] +}); // Create the Hono app const app = new Hono(); @@ -56,13 +66,11 @@ app.use( app.get('/health', c => c.json({ status: 'ok' })); // MCP endpoint -app.all('/mcp', c => transport.handleRequest(c.req.raw)); +app.all('/mcp', c => mcpHandler(c.req.raw)); // Start the server const PORT = process.env.MCP_PORT ? Number.parseInt(process.env.MCP_PORT, 10) : 3000; -await server.connect(transport); - console.log(`Starting Hono MCP server on port ${PORT}`); console.log(`Health check: http://localhost:${PORT}/health`); console.log(`MCP endpoint: http://localhost:${PORT}/mcp`); diff --git a/examples/server/src/serverGuide.examples.ts b/examples/server/src/serverGuide.examples.ts index 5a4712f83..ea926a8cc 100644 --- a/examples/server/src/serverGuide.examples.ts +++ b/examples/server/src/serverGuide.examples.ts @@ -410,8 +410,8 @@ function registerTool_roots(server: McpServer) { description: 'List files across all workspace roots', inputSchema: z.object({}) }, - async (_args, _ctx): Promise => { - const { roots } = await server.server.listRoots(); + async (_args, ctx): Promise => { + 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 }] }; } diff --git a/examples/server/src/simpleStatelessStreamableHttp.ts b/examples/server/src/simpleStatelessStreamableHttp.ts index 2b4f0363d..92a175528 100644 --- a/examples/server/src/simpleStatelessStreamableHttp.ts +++ b/examples/server/src/simpleStatelessStreamableHttp.ts @@ -94,21 +94,24 @@ const getServer = () => { return server; }; +// One shared server + one connected transport for 2026-06 clients. The +// transport's per-message router (SEP-2567) dispatches 2026-06 requests via +// the stateless path. NOTE: a single shared instance is NOT safe for +// concurrent pre-2026 clients (the legacy `initialize` handshake writes +// per-connection state on the instance). For pre-2026 clients, use +// per-instance isolation: a per-session transport as in +// `simpleStreamableHttp.ts`, or a fresh server per request. +const server = getServer(); +const transport: NodeStreamableHTTPServerTransport = new NodeStreamableHTTPServerTransport({ + sessionIdGenerator: undefined +}); +await server.connect(transport); + const app = createMcpExpressApp(); app.post('/mcp', async (req: Request, res: Response) => { - const server = getServer(); try { - const transport: NodeStreamableHTTPServerTransport = new NodeStreamableHTTPServerTransport({ - sessionIdGenerator: undefined - }); - await server.connect(transport); await transport.handleRequest(req, res, req.body); - res.on('close', () => { - console.log('Request closed'); - transport.close(); - server.close(); - }); } catch (error) { console.error('Error handling MCP request:', error); if (!res.headersSent) { diff --git a/examples/server/src/toolWithSampleServer.ts b/examples/server/src/toolWithSampleServer.ts index f6b053cf2..cb4f4fe0a 100644 --- a/examples/server/src/toolWithSampleServer.ts +++ b/examples/server/src/toolWithSampleServer.ts @@ -18,9 +18,13 @@ mcpServer.registerTool( text: z.string().describe('Text to summarize') }) }, - async ({ text }) => { - // Call the LLM through MCP sampling - const response = await mcpServer.server.createMessage({ + async ({ text }, ctx) => { + // Call the LLM through MCP sampling. + // ctx.mcpReq.requestSampling works under both the pre-2026 connection model + // (sends sampling/createMessage to the connected client) and the 2026 + // stateless model (returns an input_required result; the client retries + // with the response embedded). See SEP-2322. + const response = await ctx.mcpReq.requestSampling({ messages: [ { role: 'user', @@ -33,14 +37,10 @@ mcpServer.registerTool( maxTokens: 500 }); - // Since we're not passing tools param to createMessage, response.content is single content + const content = response.content; + const summary = content.type === 'text' ? content.text : 'Unable to generate summary'; return { - content: [ - { - type: 'text', - text: response.content.type === 'text' ? response.content.text : 'Unable to generate summary' - } - ] + content: [{ type: 'text', text: summary }] }; } );