From d000de8232c0d727f252ceddccc22cadc149879d Mon Sep 17 00:00:00 2001 From: Felix Weinberger <3823880+felixweinberger@users.noreply.github.com> Date: Thu, 2 Jul 2026 10:49:12 +0100 Subject: [PATCH 1/6] Update README.md (#2411) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 6dbdeca800..22404e5f68 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ > [!WARNING] -> **We're limiting pull requests to contributors while we land the [2026-07-28 spec](https://blog.modelcontextprotocol.io/posts/2026-07-28-release-candidate/) implementation.** +> **We're limiting pull requests to 1 per new contributor while we land the [2026-07-28 spec](https://blog.modelcontextprotocol.io/posts/2026-07-28-release-candidate/) implementation.** > > [Issues](https://github.com/modelcontextprotocol/typescript-sdk/issues/new?template=v2-feedback.yml) are the most useful feedback right now — we'll reopen PRs as v2 stabilizes. From ef120b2be0c3c3d80468c3d4a9f79be30bb0c0a3 Mon Sep 17 00:00:00 2001 From: Felix Weinberger <3823880+felixweinberger@users.noreply.github.com> Date: Thu, 2 Jul 2026 12:07:06 +0100 Subject: [PATCH 2/6] chore: add missing changesets for CommonJS builds and codemod iterations (#2412) --- .changeset/cjs-dual-builds.md | 13 +++++++++++++ .changeset/codemod-iterations-5.md | 5 +++++ 2 files changed, 18 insertions(+) create mode 100644 .changeset/cjs-dual-builds.md create mode 100644 .changeset/codemod-iterations-5.md diff --git a/.changeset/cjs-dual-builds.md b/.changeset/cjs-dual-builds.md new file mode 100644 index 0000000000..e85f418af3 --- /dev/null +++ b/.changeset/cjs-dual-builds.md @@ -0,0 +1,13 @@ +--- +'@modelcontextprotocol/server': minor +'@modelcontextprotocol/client': minor +'@modelcontextprotocol/core': minor +'@modelcontextprotocol/server-legacy': minor +'@modelcontextprotocol/codemod': minor +'@modelcontextprotocol/express': minor +'@modelcontextprotocol/hono': minor +'@modelcontextprotocol/fastify': minor +'@modelcontextprotocol/node': minor +--- + +Ship CommonJS builds alongside ESM for all v2 packages, so `require()` consumers and CJS-only toolchains can use the SDK without a bundler shim. diff --git a/.changeset/codemod-iterations-5.md b/.changeset/codemod-iterations-5.md new file mode 100644 index 0000000000..fe6f5e8499 --- /dev/null +++ b/.changeset/codemod-iterations-5.md @@ -0,0 +1,5 @@ +--- +'@modelcontextprotocol/codemod': patch +--- + +v1-to-v2 migration fixes from continued real-world migrations (codemod iterations 5). From 448ba0ff54b10a63260bc5684d459e4cea9df8d2 Mon Sep 17 00:00:00 2001 From: Felix Weinberger <3823880+felixweinberger@users.noreply.github.com> Date: Thu, 2 Jul 2026 12:20:43 +0100 Subject: [PATCH 3/6] chore: drop duplicate CommonJS changeset (#2414) --- .changeset/cjs-dual-builds.md | 13 ------------- 1 file changed, 13 deletions(-) delete mode 100644 .changeset/cjs-dual-builds.md diff --git a/.changeset/cjs-dual-builds.md b/.changeset/cjs-dual-builds.md deleted file mode 100644 index e85f418af3..0000000000 --- a/.changeset/cjs-dual-builds.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -'@modelcontextprotocol/server': minor -'@modelcontextprotocol/client': minor -'@modelcontextprotocol/core': minor -'@modelcontextprotocol/server-legacy': minor -'@modelcontextprotocol/codemod': minor -'@modelcontextprotocol/express': minor -'@modelcontextprotocol/hono': minor -'@modelcontextprotocol/fastify': minor -'@modelcontextprotocol/node': minor ---- - -Ship CommonJS builds alongside ESM for all v2 packages, so `require()` consumers and CJS-only toolchains can use the SDK without a bundler shim. From eacf2e5dbeb1fa65492750c15fdc55cafdf28686 Mon Sep 17 00:00:00 2001 From: Felix Weinberger <3823880+felixweinberger@users.noreply.github.com> Date: Thu, 2 Jul 2026 13:12:49 +0100 Subject: [PATCH 4/6] Add --protocol-version flag to cli-client example (#2406) --- examples/cli-client/README.md | 19 ++++++------ examples/cli-client/cli.ts | 36 ++++++++++++---------- examples/cli-client/host/host.ts | 43 ++++++++++++++++++++++++--- examples/cli-client/test/host.test.ts | 23 ++++++++++++++ 4 files changed, 92 insertions(+), 29 deletions(-) diff --git a/examples/cli-client/README.md b/examples/cli-client/README.md index c9eca01ccb..c75d086e12 100644 --- a/examples/cli-client/README.md +++ b/examples/cli-client/README.md @@ -48,7 +48,7 @@ pnpm --filter @mcp-examples/todos-server start:http ANTHROPIC_API_KEY=sk-… pnpm --filter @mcp-examples/cli-client start -- --server http://127.0.0.1:3000/mcp --provider anthropic ``` -The status line shows what was negotiated — `connected to "todos" (2026-07-28, 8 tools, 2 resources, 2 prompts)`. Add `--legacy` in terminal B to force the 2025-era handshake against the same server and watch the legacy arms of every feature run instead (`connected to "todos" (2025-11-25, …)`). +The status line shows what was negotiated — `connected to "todos" (2026-07-28, 8 tools, 2 resources, 2 prompts)`. Add `--legacy` in terminal B to force the 2025-era handshake against the same server and watch the legacy arms of every feature run instead (`connected to "todos" (2025-11-25, …)`). To hold the connection to one exact revision, use `--protocol-version 2025-06-18` (or any supported revision) — the connection fails rather than settle on anything else. A tour that touches everything, in one sitting: @@ -111,14 +111,15 @@ For a persistent setup, copy `config.example.json` to `config.json` (or pass `-- ## All flags ```text ---server connect to just this server: an http(s) URL (OAuth on demand) or a stdio command line (repeatable) ---config mcpServers config file (default: ./config.json, falling back to spawning todos-server) ---provider scripted | anthropic | openai | gemini (default: first one with a key in the env, else scripted) ---model pin a model id (default: the provider's latest mid-tier model) ---root workspace root exposed to servers via roots/list (repeatable; default: cwd) ---callback-port fixed loopback port for the OAuth callback (default: a free port) ---legacy use the 2025 initialize handshake instead of probing for 2026-07-28 --h, --help show usage +--server connect to just this server: an http(s) URL (OAuth on demand) or a stdio command line (repeatable) +--config mcpServers config file (default: ./config.json, falling back to spawning todos-server) +--provider scripted | anthropic | openai | gemini (default: first one with a key in the env, else scripted) +--model pin a model id (default: the provider's latest mid-tier model) +--root workspace root exposed to servers via roots/list (repeatable; default: cwd) +--callback-port fixed loopback port for the OAuth callback (default: a free port) +--legacy use the 2025 initialize handshake instead of probing for 2026-07-28 +--protocol-version negotiate exactly this revision: 2025-era values (e.g. 2025-06-18) via the legacy handshake, 2026-07-28+ via a modern pin +-h, --help show usage ``` ## How this example is tested diff --git a/examples/cli-client/cli.ts b/examples/cli-client/cli.ts index 84b13df8ab..5b29c5a848 100644 --- a/examples/cli-client/cli.ts +++ b/examples/cli-client/cli.ts @@ -24,14 +24,15 @@ import type { LLMProvider } from './providers/provider'; import { ScriptedProvider } from './providers/scripted'; const USAGE = `usage: tsx cli.ts [options] - --server connect to just this server: an http(s) URL (OAuth on demand) or a stdio command line (repeatable) - --config mcpServers config file (default: ./config.json, falling back to spawning the sibling todos-server) - --provider scripted | anthropic | openai | gemini (default: first one with an API key in the env, else scripted) - --model pin a model id (default: the provider's latest mid-tier model) - --root workspace root exposed to servers via roots/list (repeatable; default: cwd) - --callback-port fixed loopback port for the OAuth callback (default: a free port; set this when port-forwarding over SSH) - --legacy use the 2025 initialize handshake instead of probing for 2026-07-28 - --help this help`; + --server connect to just this server: an http(s) URL (OAuth on demand) or a stdio command line (repeatable) + --config mcpServers config file (default: ./config.json, falling back to spawning the sibling todos-server) + --provider scripted | anthropic | openai | gemini (default: first one with an API key in the env, else scripted) + --model pin a model id (default: the provider's latest mid-tier model) + --root workspace root exposed to servers via roots/list (repeatable; default: cwd) + --callback-port fixed loopback port for the OAuth callback (default: a free port; set this when port-forwarding over SSH) + --legacy use the 2025 initialize handshake instead of probing for 2026-07-28 + --protocol-version negotiate exactly this revision: 2025-era values (e.g. 2025-06-18) via the legacy handshake, 2026-07-28+ via a modern pin + --help this help`; function pickProvider(name: string | undefined, model: string | undefined): LLMProvider { const chosen = @@ -73,6 +74,7 @@ const { values } = parseArgs({ root: { type: 'string', multiple: true }, 'callback-port': { type: 'string' }, legacy: { type: 'boolean' }, + 'protocol-version': { type: 'string' }, help: { type: 'boolean', short: 'h' } } }); @@ -111,15 +113,17 @@ for (const [serverName, entry] of Object.entries(config.mcpServers)) { ui.status(` ${serverName} → ${'url' in entry ? entry.url : [entry.command, ...(entry.args ?? [])].join(' ')}`); } -const host = new McpHost({ - ui, - provider, - roots: values.root ?? [process.cwd()], - legacy: values.legacy ?? false, - oauthCallbackPort: values['callback-port'] ? Number.parseInt(values['callback-port'], 10) : undefined -}); -hostRef.current = host; +let host: McpHost; try { + host = new McpHost({ + ui, + provider, + roots: values.root ?? [process.cwd()], + legacy: values.legacy ?? false, + protocolVersion: values['protocol-version'], + oauthCallbackPort: values['callback-port'] ? Number.parseInt(values['callback-port'], 10) : undefined + }); + hostRef.current = host; await host.connect(config); } catch (error) { ui.print(error instanceof Error ? error.message : String(error)); diff --git a/examples/cli-client/host/host.ts b/examples/cli-client/host/host.ts index 15d179b217..49165c9554 100644 --- a/examples/cli-client/host/host.ts +++ b/examples/cli-client/host/host.ts @@ -10,7 +10,8 @@ import type { Prompt, Resource, ResourceTemplateType, - Tool + Tool, + VersionNegotiationOptions } from '@modelcontextprotocol/client'; import { Client, @@ -18,6 +19,7 @@ import { ProtocolError, SdkError, StreamableHTTPClientTransport, + SUPPORTED_PROTOCOL_VERSIONS, UnauthorizedError } from '@modelcontextprotocol/client'; import { StdioClientTransport } from '@modelcontextprotocol/client/stdio'; @@ -61,10 +63,43 @@ export interface McpHostOptions { roots?: string[]; /** Use the 2025 `initialize` handshake instead of probing for 2026-07-28. */ legacy?: boolean; + /** + * Negotiate exactly this protocol revision: a known 2025-era value runs the legacy + * handshake offering only that revision; anything else is pinned via the modern + * handshake, which fails loudly unless the server offers it. + */ + protocolVersion?: string; /** Fixed loopback port for the OAuth callback (default: an OS-assigned free port). Useful over SSH port-forwarding. */ oauthCallbackPort?: number; } +/** The version-negotiation slice of the SDK client options every connection this host makes shares. */ +export interface VersionOptions { + versionNegotiation: VersionNegotiationOptions; + supportedProtocolVersions?: string[]; +} + +/** + * Map the era toggle and optional pinned revision onto the SDK's negotiation options. + * A known 2025-era revision runs the legacy handshake offering exactly that revision + * (the client rejects a server that answers with any other version); everything else + * becomes a modern pin, and the SDK's own typed error covers strings that are neither. + */ +export function resolveVersionOptions(legacy: boolean, protocolVersion?: string): VersionOptions { + if (protocolVersion === undefined) { + return { versionNegotiation: { mode: legacy ? 'legacy' : 'auto' } }; + } + if (SUPPORTED_PROTOCOL_VERSIONS.includes(protocolVersion)) { + return { versionNegotiation: { mode: 'legacy' }, supportedProtocolVersions: [protocolVersion] }; + } + if (legacy) { + throw new Error( + `--legacy conflicts with --protocol-version ${protocolVersion}: the 2025 handshake can only negotiate ${SUPPORTED_PROTOCOL_VERSIONS.join(', ')}` + ); + } + return { versionNegotiation: { mode: { pin: protocolVersion } } }; +} + function unwrapUnauthorized(error: unknown): UnauthorizedError | undefined { if (error instanceof UnauthorizedError) return error; // Under versionNegotiation 'auto', a connect-time 401 surfaces as @@ -94,7 +129,7 @@ function samplingContentToParts(content: CreateMessageRequest['params']['message export class McpHost { private readonly ui: HostUI; private readonly provider: LLMProvider; - private readonly legacy: boolean; + private readonly versionOptions: VersionOptions; private roots: string[]; private readonly watches: McpSubscription[] = []; private readonly oauthCallbackPort?: number; @@ -103,7 +138,7 @@ export class McpHost { constructor(options: McpHostOptions) { this.ui = options.ui; this.provider = options.provider; - this.legacy = options.legacy ?? false; + this.versionOptions = resolveVersionOptions(options.legacy ?? false, options.protocolVersion); this.oauthCallbackPort = options.oauthCallbackPort; this.roots = (options.roots ?? [process.cwd()]).map(root => path.resolve(root)); } @@ -309,7 +344,7 @@ export class McpHost { private buildClient(name: string): Client { const client = new Client(CLIENT_INFO, { - versionNegotiation: { mode: this.legacy ? 'legacy' : 'auto' }, + ...this.versionOptions, capabilities: { // Both elicitation modes are declared because the handler below implements both. elicitation: { form: {}, url: {} }, diff --git a/examples/cli-client/test/host.test.ts b/examples/cli-client/test/host.test.ts index 428b79a820..8ff28d1e02 100644 --- a/examples/cli-client/test/host.test.ts +++ b/examples/cli-client/test/host.test.ts @@ -2,6 +2,7 @@ import { describe, expect, it } from 'vitest'; import { configFromTargets, interpolateEnv, isHttpServer, parseConfig } from '../host/config'; import { contentBlockToParts, resourceToContextText, stripAnsi, toolResultToParts, truncate } from '../host/content'; +import { resolveVersionOptions } from '../host/host'; import { namespaceTool, routeNamespacedTool, sanitizeServerName } from '../host/naming'; describe('tool namespacing and routing', () => { @@ -94,3 +95,25 @@ describe('config parsing', () => { expect(() => configFromTargets([])).toThrow(); }); }); + +describe('protocol version selection', () => { + it('defaults to auto probing, --legacy to the plain 2025 handshake', () => { + expect(resolveVersionOptions(false)).toEqual({ versionNegotiation: { mode: 'auto' } }); + expect(resolveVersionOptions(true)).toEqual({ versionNegotiation: { mode: 'legacy' } }); + }); + + it('runs a known 2025-era revision through the legacy handshake, offering only that revision', () => { + const expected = { versionNegotiation: { mode: 'legacy' }, supportedProtocolVersions: ['2025-06-18'] }; + expect(resolveVersionOptions(false, '2025-06-18')).toEqual(expected); + // --legacy alongside a 2025-era revision is redundant but consistent. + expect(resolveVersionOptions(true, '2025-06-18')).toEqual(expected); + }); + + it('pins anything newer via the modern handshake', () => { + expect(resolveVersionOptions(false, '2026-07-28')).toEqual({ versionNegotiation: { mode: { pin: '2026-07-28' } } }); + }); + + it('rejects --legacy combined with a revision the 2025 handshake cannot reach', () => { + expect(() => resolveVersionOptions(true, '2026-07-28')).toThrow(/--legacy conflicts with --protocol-version/); + }); +}); From 1ea960b5bbf1d2081d61575467690b8f38f96c1f Mon Sep 17 00:00:00 2001 From: Felix Weinberger <3823880+felixweinberger@users.noreply.github.com> Date: Thu, 2 Jul 2026 13:16:52 +0100 Subject: [PATCH 5/6] chore(examples): bump better-auth to ^1.6.2 (#2415) --- examples/shared/package.json | 2 +- pnpm-lock.yaml | 190 ++++++++++++++++++----------------- 2 files changed, 100 insertions(+), 92 deletions(-) diff --git a/examples/shared/package.json b/examples/shared/package.json index 71bc9c34a1..95205b6135 100644 --- a/examples/shared/package.json +++ b/examples/shared/package.json @@ -34,7 +34,7 @@ "dependencies": { "@modelcontextprotocol/server": "workspace:^", "@modelcontextprotocol/express": "workspace:^", - "better-auth": "^1.4.17", + "better-auth": "^1.6.2", "better-sqlite3": "^12.6.2", "cors": "catalog:runtimeServerOnly", "express": "catalog:runtimeServerOnly" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9979795255..496cfefe2b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -966,8 +966,8 @@ importers: specifier: workspace:^ version: link:../../packages/server better-auth: - specifier: ^1.4.17 - version: 1.5.6(@opentelemetry/api@1.9.1)(better-sqlite3@12.8.0)(vitest@4.1.2(@opentelemetry/api@1.9.1)(@types/node@25.5.0)(vite@7.3.0(@types/node@25.5.0)(tsx@4.21.0)(yaml@2.8.3)))(vue@3.5.38(typescript@5.9.3)) + specifier: ^1.6.2 + version: 1.6.20(@opentelemetry/api@1.9.1)(better-sqlite3@12.8.0)(vitest@4.1.2(@opentelemetry/api@1.9.1)(@types/node@25.5.0)(vite@7.3.0(@types/node@25.5.0)(tsx@4.21.0)(yaml@2.8.3)))(vue@3.5.38(typescript@5.9.3)) better-sqlite3: specifier: ^12.6.2 version: 12.8.0 @@ -2188,62 +2188,64 @@ packages: resolution: {integrity: sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==} engines: {node: '>=6.9.0'} - '@better-auth/core@1.5.6': - resolution: {integrity: sha512-Ez9DZdIMFyxHremmoLz1emFPGNQomDC1jqqBPnZ6Ci+6TiGN3R9w/Y03cJn6I8r1ycKgOzeVMZtJ/erOZ27Gsw==} + '@better-auth/core@1.6.20': + resolution: {integrity: sha512-y73I1xNXuNYiHBFduWGRcJ2ro2rNuVDEYkgVMJtIaRXtbosdXHs9gfyQrHecgeHMHKx1SYSBT/CExak0vVMTng==} peerDependencies: - '@better-auth/utils': 0.3.1 - '@better-fetch/fetch': 1.1.21 + '@better-auth/utils': 0.4.2 + '@better-fetch/fetch': 1.3.1 '@cloudflare/workers-types': '>=4' '@opentelemetry/api': ^1.9.0 - better-call: 1.3.2 + better-call: 1.3.6 jose: ^6.1.0 - kysely: ^0.28.5 + kysely: ^0.28.5 || ^0.29.0 nanostores: ^1.0.1 peerDependenciesMeta: '@cloudflare/workers-types': optional: true + '@opentelemetry/api': + optional: true - '@better-auth/drizzle-adapter@1.5.6': - resolution: {integrity: sha512-VfFFmaoFw3ug12SiSuIwzrMoHyIVmkMGWm9gZ4sXdYYVX4HboCL4m3fjzOhppcmK5OGatRuU+N1UX6wxCITcXw==} + '@better-auth/drizzle-adapter@1.6.20': + resolution: {integrity: sha512-hJHfCdAiZrC7EmZAt3NAiGgcNo9Y5Qz3PLL+a9rODXaAJGCMvzUJniqef9wHuJBwU0SWW+2f4wXe8xQmaC/IKQ==} peerDependencies: - '@better-auth/core': 1.5.6 - '@better-auth/utils': ^0.3.0 - drizzle-orm: '>=0.41.0' + '@better-auth/core': ^1.6.20 + '@better-auth/utils': 0.4.2 + drizzle-orm: ^0.45.2 peerDependenciesMeta: drizzle-orm: optional: true - '@better-auth/kysely-adapter@1.5.6': - resolution: {integrity: sha512-Fnf+h8WVKtw6lEOmVmiVVzDf3shJtM60AYf9XTnbdCeUd6MxN/KnaJZpkgtYnRs7a+nwtkVB+fg4lGETebGFXQ==} + '@better-auth/kysely-adapter@1.6.20': + resolution: {integrity: sha512-Uvpmgbx5y8JqXroVanNzDdKzOl3HojoTz+/X6MR6zOUr25IzlYz660mjnu0rxKiIF55kD3CroqFsDzjNUw7ERw==} peerDependencies: - '@better-auth/core': 1.5.6 - '@better-auth/utils': ^0.3.0 - kysely: ^0.27.0 || ^0.28.0 + '@better-auth/core': ^1.6.20 + '@better-auth/utils': 0.4.2 + kysely: ^0.28.17 || ^0.29.0 peerDependenciesMeta: kysely: optional: true - '@better-auth/memory-adapter@1.5.6': - resolution: {integrity: sha512-rS7ZsrIl5uvloUgNN0u9LOZJMMXnsZXVdUZ3MrTBKWM2KpoJjzPr9yN3Szyma5+0V7SltnzSGHPkYj2bEzzmlA==} + '@better-auth/memory-adapter@1.6.20': + resolution: {integrity: sha512-J5Ni0LlFijbzXlwu2rFHaD8zEFocmajyzWkRnHsq8LhV/Dk4iWQwwnqzLrPoDQEj8roECAUF03hrIeMzqWRqJQ==} peerDependencies: - '@better-auth/core': 1.5.6 - '@better-auth/utils': ^0.3.0 + '@better-auth/core': ^1.6.20 + '@better-auth/utils': 0.4.2 - '@better-auth/mongo-adapter@1.5.6': - resolution: {integrity: sha512-6+M3MS2mor8fTUV3EI1FBLP0cs6QfbN+Ovx9+XxR/GdfKIBoNFzmPEPRbdGt+ft6PvrITsUm+T70+kkHgVSP6w==} + '@better-auth/mongo-adapter@1.6.20': + resolution: {integrity: sha512-ClDBJf6h4g85WJswxwQwxLaiyRU67Gmz/uaIf19tY1gqlLJDykSGjmqRNSBMG5rWABNzcNqbO4KG31rYUldbIw==} peerDependencies: - '@better-auth/core': 1.5.6 - '@better-auth/utils': ^0.3.0 + '@better-auth/core': ^1.6.20 + '@better-auth/utils': 0.4.2 mongodb: ^6.0.0 || ^7.0.0 peerDependenciesMeta: mongodb: optional: true - '@better-auth/prisma-adapter@1.5.6': - resolution: {integrity: sha512-UxY9vQJs1Tt+O+T2YQnseDMlWmUSQvFZSBb5YiFRg7zcm+TEzujh4iX2/csA0YiZptLheovIuVWTP9nriewEBA==} + '@better-auth/prisma-adapter@1.6.20': + resolution: {integrity: sha512-WhYdhSGuVSfu1peCSf2snmmVzfWjRaEvbSrsNCusiwGE9l94HlES4mjSPM48fed24hL7yg4j1dYK/yjEt87FpQ==} peerDependencies: - '@better-auth/core': 1.5.6 - '@better-auth/utils': ^0.3.0 + '@better-auth/core': ^1.6.20 + '@better-auth/utils': 0.4.2 '@prisma/client': ^5.0.0 || ^6.0.0 || ^7.0.0 prisma: ^5.0.0 || ^6.0.0 || ^7.0.0 peerDependenciesMeta: @@ -2252,16 +2254,18 @@ packages: prisma: optional: true - '@better-auth/telemetry@1.5.6': - resolution: {integrity: sha512-yXC7NSxnIFlxDkGdpD7KA+J9nqIQAPCJKe77GoaC5bWoe/DALo1MYorZfTgOafS7wrslNtsPT4feV/LJi1ubqQ==} + '@better-auth/telemetry@1.6.20': + resolution: {integrity: sha512-3BhbY3naQDERvdJvJ7fGszVY6rpsVfc6c9uyBVZlC1coVEF/rkM0rIcjtMVI1GUH7vWy1wjR6qF5vQnMun3XNQ==} peerDependencies: - '@better-auth/core': 1.5.6 + '@better-auth/core': ^1.6.20 + '@better-auth/utils': 0.4.2 + '@better-fetch/fetch': 1.3.1 - '@better-auth/utils@0.3.1': - resolution: {integrity: sha512-+CGp4UmZSUrHHnpHhLPYu6cV+wSUSvVbZbNykxhUDocpVNTo9uFFxw/NqJlh1iC4wQ9HKKWGCKuZ5wUgS0v6Kg==} + '@better-auth/utils@0.4.2': + resolution: {integrity: sha512-AUxrvu+HaaODsUyzDxFgwd/8RZ1yZaYo42LXKSrU2oGgR38pS1ij8nqQKNgtTWoYGpNevNXtCfgTy6loHveW9A==} - '@better-fetch/fetch@1.1.21': - resolution: {integrity: sha512-/ImESw0sskqlVR94jB+5+Pxjf+xBwDZF/N5+y2/q4EqD7IARUTSpPfIo8uf39SYpCxyOCtbyYpUrZ3F/k0zT4A==} + '@better-fetch/fetch@1.3.1': + resolution: {integrity: sha512-ABkD1WhyfPZprKRQI3bhATjeiFuNWC9PXhfGWqL+sg/gKrM977oFrYkdb4msM3hgUGonr7KlOsOFT5TU2rht9g==} '@cfworker/json-schema@4.1.1': resolution: {integrity: sha512-gAmrUZSGtKc3AiBL71iNWxDsyUC5uMaKKGdvzYsBoTW/xi42JQHl7eKV2OYzCUqvc+D2RCcf7EXY2iCyFIk6og==} @@ -4243,8 +4247,8 @@ packages: before-after-hook@4.0.0: resolution: {integrity: sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ==} - better-auth@1.5.6: - resolution: {integrity: sha512-QSpJTqaT1XVfWRQe/fm3PgeuwOIlz1nWX/Dx7nsHStJ382bLzmDbQk2u7IT0IJ6wS5SRxfqEE1Ev9TXontgyAQ==} + better-auth@1.6.20: + resolution: {integrity: sha512-fSpGHGRKiGRiYVd3QTQtuVZ8oxpiSe/7ip0Rpvt/Sy8zQbEbVKUPMOhE0gLXg+FjqTUsIo7582hxUYxtEcqUpA==} peerDependencies: '@lynx-js/react': '*' '@prisma/client': ^5.0.0 || ^6.0.0 || ^7.0.0 @@ -4253,7 +4257,7 @@ packages: '@tanstack/solid-start': ^1.0.0 better-sqlite3: ^12.0.0 drizzle-kit: '>=0.31.4' - drizzle-orm: '>=0.41.0' + drizzle-orm: ^0.45.2 mongodb: ^6.0.0 || ^7.0.0 mysql2: ^3.0.0 next: ^14.0.0 || ^15.0.0 || ^16.0.0 @@ -4305,8 +4309,8 @@ packages: vue: optional: true - better-call@1.3.2: - resolution: {integrity: sha512-4cZIfrerDsNTn3cm+MhLbUePN0gdwkhSXEuG7r/zuQ8c/H7iU0/jSK5TD3FW7U0MgKHce/8jGpPYNO4Ve+4NBw==} + better-call@1.3.6: + resolution: {integrity: sha512-no1jI+h6Bkxs1NVBo4rONbVIzsPjZ8IUu7IHaJBiFwVX1XEQGN8KpHots5fSWmXe9nNyLuLIcgx6WEUcE6EDaA==} peerDependencies: zod: ^4.0.0 peerDependenciesMeta: @@ -5466,9 +5470,9 @@ packages: resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} engines: {node: '>=6'} - kysely@0.28.14: - resolution: {integrity: sha512-SU3lgh0rPvq7upc6vvdVrCsSMUG1h3ChvHVOY7wJ2fw4C9QEB7X3d5eyYEyULUX7UQtxZJtZXGuT6U2US72UYA==} - engines: {node: '>=20.0.0'} + kysely@0.29.2: + resolution: {integrity: sha512-s6WVJyEZrbm6jhBpiKHsGHyePMrVQKJ85wZCFCr9W4QHv6WTjWIrdvTmO9hDEA3bNK0xkrE2DqrHsXMLWuZpQg==} + engines: {node: '>=22.0.0'} lefthook-darwin-arm64@2.1.4: resolution: {integrity: sha512-BUAAE9+rUrjr39a+wH/1zHmGrDdwUQ2Yq/z6BQbM/yUb9qtXBRcQ5eOXxApqWW177VhGBpX31aqIlfAZ5Q7wzw==} @@ -7005,55 +7009,58 @@ snapshots: '@babel/helper-string-parser': 7.29.7 '@babel/helper-validator-identifier': 7.29.7 - '@better-auth/core@1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.1)(better-call@1.3.2(zod@4.3.6))(jose@6.2.2)(kysely@0.28.14)(nanostores@1.2.0)': + '@better-auth/core@1.6.20(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.3.6(zod@4.3.6))(jose@6.2.2)(kysely@0.29.2)(nanostores@1.2.0)': dependencies: - '@better-auth/utils': 0.3.1 - '@better-fetch/fetch': 1.1.21 - '@opentelemetry/api': 1.9.1 + '@better-auth/utils': 0.4.2 + '@better-fetch/fetch': 1.3.1 '@opentelemetry/semantic-conventions': 1.40.0 '@standard-schema/spec': 1.1.0 - better-call: 1.3.2(zod@4.3.6) + better-call: 1.3.6(zod@4.3.6) jose: 6.2.2 - kysely: 0.28.14 + kysely: 0.29.2 nanostores: 1.2.0 zod: 4.3.6 + optionalDependencies: + '@opentelemetry/api': 1.9.1 - '@better-auth/drizzle-adapter@1.5.6(@better-auth/core@1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.1)(better-call@1.3.2(zod@4.3.6))(jose@6.2.2)(kysely@0.28.14)(nanostores@1.2.0))(@better-auth/utils@0.3.1)': + '@better-auth/drizzle-adapter@1.6.20(@better-auth/core@1.6.20(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.3.6(zod@4.3.6))(jose@6.2.2)(kysely@0.29.2)(nanostores@1.2.0))(@better-auth/utils@0.4.2)': dependencies: - '@better-auth/core': 1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.1)(better-call@1.3.2(zod@4.3.6))(jose@6.2.2)(kysely@0.28.14)(nanostores@1.2.0) - '@better-auth/utils': 0.3.1 + '@better-auth/core': 1.6.20(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.3.6(zod@4.3.6))(jose@6.2.2)(kysely@0.29.2)(nanostores@1.2.0) + '@better-auth/utils': 0.4.2 - '@better-auth/kysely-adapter@1.5.6(@better-auth/core@1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.1)(better-call@1.3.2(zod@4.3.6))(jose@6.2.2)(kysely@0.28.14)(nanostores@1.2.0))(@better-auth/utils@0.3.1)(kysely@0.28.14)': + '@better-auth/kysely-adapter@1.6.20(@better-auth/core@1.6.20(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.3.6(zod@4.3.6))(jose@6.2.2)(kysely@0.29.2)(nanostores@1.2.0))(@better-auth/utils@0.4.2)(kysely@0.29.2)': dependencies: - '@better-auth/core': 1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.1)(better-call@1.3.2(zod@4.3.6))(jose@6.2.2)(kysely@0.28.14)(nanostores@1.2.0) - '@better-auth/utils': 0.3.1 + '@better-auth/core': 1.6.20(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.3.6(zod@4.3.6))(jose@6.2.2)(kysely@0.29.2)(nanostores@1.2.0) + '@better-auth/utils': 0.4.2 optionalDependencies: - kysely: 0.28.14 + kysely: 0.29.2 - '@better-auth/memory-adapter@1.5.6(@better-auth/core@1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.1)(better-call@1.3.2(zod@4.3.6))(jose@6.2.2)(kysely@0.28.14)(nanostores@1.2.0))(@better-auth/utils@0.3.1)': + '@better-auth/memory-adapter@1.6.20(@better-auth/core@1.6.20(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.3.6(zod@4.3.6))(jose@6.2.2)(kysely@0.29.2)(nanostores@1.2.0))(@better-auth/utils@0.4.2)': dependencies: - '@better-auth/core': 1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.1)(better-call@1.3.2(zod@4.3.6))(jose@6.2.2)(kysely@0.28.14)(nanostores@1.2.0) - '@better-auth/utils': 0.3.1 + '@better-auth/core': 1.6.20(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.3.6(zod@4.3.6))(jose@6.2.2)(kysely@0.29.2)(nanostores@1.2.0) + '@better-auth/utils': 0.4.2 - '@better-auth/mongo-adapter@1.5.6(@better-auth/core@1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.1)(better-call@1.3.2(zod@4.3.6))(jose@6.2.2)(kysely@0.28.14)(nanostores@1.2.0))(@better-auth/utils@0.3.1)': + '@better-auth/mongo-adapter@1.6.20(@better-auth/core@1.6.20(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.3.6(zod@4.3.6))(jose@6.2.2)(kysely@0.29.2)(nanostores@1.2.0))(@better-auth/utils@0.4.2)': dependencies: - '@better-auth/core': 1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.1)(better-call@1.3.2(zod@4.3.6))(jose@6.2.2)(kysely@0.28.14)(nanostores@1.2.0) - '@better-auth/utils': 0.3.1 + '@better-auth/core': 1.6.20(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.3.6(zod@4.3.6))(jose@6.2.2)(kysely@0.29.2)(nanostores@1.2.0) + '@better-auth/utils': 0.4.2 - '@better-auth/prisma-adapter@1.5.6(@better-auth/core@1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.1)(better-call@1.3.2(zod@4.3.6))(jose@6.2.2)(kysely@0.28.14)(nanostores@1.2.0))(@better-auth/utils@0.3.1)': + '@better-auth/prisma-adapter@1.6.20(@better-auth/core@1.6.20(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.3.6(zod@4.3.6))(jose@6.2.2)(kysely@0.29.2)(nanostores@1.2.0))(@better-auth/utils@0.4.2)': dependencies: - '@better-auth/core': 1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.1)(better-call@1.3.2(zod@4.3.6))(jose@6.2.2)(kysely@0.28.14)(nanostores@1.2.0) - '@better-auth/utils': 0.3.1 + '@better-auth/core': 1.6.20(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.3.6(zod@4.3.6))(jose@6.2.2)(kysely@0.29.2)(nanostores@1.2.0) + '@better-auth/utils': 0.4.2 - '@better-auth/telemetry@1.5.6(@better-auth/core@1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.1)(better-call@1.3.2(zod@4.3.6))(jose@6.2.2)(kysely@0.28.14)(nanostores@1.2.0))': + '@better-auth/telemetry@1.6.20(@better-auth/core@1.6.20(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.3.6(zod@4.3.6))(jose@6.2.2)(kysely@0.29.2)(nanostores@1.2.0))(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)': dependencies: - '@better-auth/core': 1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.1)(better-call@1.3.2(zod@4.3.6))(jose@6.2.2)(kysely@0.28.14)(nanostores@1.2.0) - '@better-auth/utils': 0.3.1 - '@better-fetch/fetch': 1.1.21 + '@better-auth/core': 1.6.20(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.3.6(zod@4.3.6))(jose@6.2.2)(kysely@0.29.2)(nanostores@1.2.0) + '@better-auth/utils': 0.4.2 + '@better-fetch/fetch': 1.3.1 - '@better-auth/utils@0.3.1': {} + '@better-auth/utils@0.4.2': + dependencies: + '@noble/hashes': 2.0.1 - '@better-fetch/fetch@1.1.21': {} + '@better-fetch/fetch@1.3.1': {} '@cfworker/json-schema@4.1.1': {} @@ -7890,7 +7897,8 @@ snapshots: dependencies: '@octokit/openapi-types': 27.0.0 - '@opentelemetry/api@1.9.1': {} + '@opentelemetry/api@1.9.1': + optional: true '@opentelemetry/semantic-conventions@1.40.0': {} @@ -8803,23 +8811,23 @@ snapshots: before-after-hook@4.0.0: {} - better-auth@1.5.6(@opentelemetry/api@1.9.1)(better-sqlite3@12.8.0)(vitest@4.1.2(@opentelemetry/api@1.9.1)(@types/node@25.5.0)(vite@7.3.0(@types/node@25.5.0)(tsx@4.21.0)(yaml@2.8.3)))(vue@3.5.38(typescript@5.9.3)): - dependencies: - '@better-auth/core': 1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.1)(better-call@1.3.2(zod@4.3.6))(jose@6.2.2)(kysely@0.28.14)(nanostores@1.2.0) - '@better-auth/drizzle-adapter': 1.5.6(@better-auth/core@1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.1)(better-call@1.3.2(zod@4.3.6))(jose@6.2.2)(kysely@0.28.14)(nanostores@1.2.0))(@better-auth/utils@0.3.1) - '@better-auth/kysely-adapter': 1.5.6(@better-auth/core@1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.1)(better-call@1.3.2(zod@4.3.6))(jose@6.2.2)(kysely@0.28.14)(nanostores@1.2.0))(@better-auth/utils@0.3.1)(kysely@0.28.14) - '@better-auth/memory-adapter': 1.5.6(@better-auth/core@1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.1)(better-call@1.3.2(zod@4.3.6))(jose@6.2.2)(kysely@0.28.14)(nanostores@1.2.0))(@better-auth/utils@0.3.1) - '@better-auth/mongo-adapter': 1.5.6(@better-auth/core@1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.1)(better-call@1.3.2(zod@4.3.6))(jose@6.2.2)(kysely@0.28.14)(nanostores@1.2.0))(@better-auth/utils@0.3.1) - '@better-auth/prisma-adapter': 1.5.6(@better-auth/core@1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.1)(better-call@1.3.2(zod@4.3.6))(jose@6.2.2)(kysely@0.28.14)(nanostores@1.2.0))(@better-auth/utils@0.3.1) - '@better-auth/telemetry': 1.5.6(@better-auth/core@1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.1)(better-call@1.3.2(zod@4.3.6))(jose@6.2.2)(kysely@0.28.14)(nanostores@1.2.0)) - '@better-auth/utils': 0.3.1 - '@better-fetch/fetch': 1.1.21 + better-auth@1.6.20(@opentelemetry/api@1.9.1)(better-sqlite3@12.8.0)(vitest@4.1.2(@opentelemetry/api@1.9.1)(@types/node@25.5.0)(vite@7.3.0(@types/node@25.5.0)(tsx@4.21.0)(yaml@2.8.3)))(vue@3.5.38(typescript@5.9.3)): + dependencies: + '@better-auth/core': 1.6.20(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.3.6(zod@4.3.6))(jose@6.2.2)(kysely@0.29.2)(nanostores@1.2.0) + '@better-auth/drizzle-adapter': 1.6.20(@better-auth/core@1.6.20(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.3.6(zod@4.3.6))(jose@6.2.2)(kysely@0.29.2)(nanostores@1.2.0))(@better-auth/utils@0.4.2) + '@better-auth/kysely-adapter': 1.6.20(@better-auth/core@1.6.20(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.3.6(zod@4.3.6))(jose@6.2.2)(kysely@0.29.2)(nanostores@1.2.0))(@better-auth/utils@0.4.2)(kysely@0.29.2) + '@better-auth/memory-adapter': 1.6.20(@better-auth/core@1.6.20(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.3.6(zod@4.3.6))(jose@6.2.2)(kysely@0.29.2)(nanostores@1.2.0))(@better-auth/utils@0.4.2) + '@better-auth/mongo-adapter': 1.6.20(@better-auth/core@1.6.20(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.3.6(zod@4.3.6))(jose@6.2.2)(kysely@0.29.2)(nanostores@1.2.0))(@better-auth/utils@0.4.2) + '@better-auth/prisma-adapter': 1.6.20(@better-auth/core@1.6.20(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.3.6(zod@4.3.6))(jose@6.2.2)(kysely@0.29.2)(nanostores@1.2.0))(@better-auth/utils@0.4.2) + '@better-auth/telemetry': 1.6.20(@better-auth/core@1.6.20(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.3.6(zod@4.3.6))(jose@6.2.2)(kysely@0.29.2)(nanostores@1.2.0))(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1) + '@better-auth/utils': 0.4.2 + '@better-fetch/fetch': 1.3.1 '@noble/ciphers': 2.1.1 '@noble/hashes': 2.0.1 - better-call: 1.3.2(zod@4.3.6) + better-call: 1.3.6(zod@4.3.6) defu: 6.1.4 jose: 6.2.2 - kysely: 0.28.14 + kysely: 0.29.2 nanostores: 1.2.0 zod: 4.3.6 optionalDependencies: @@ -8830,10 +8838,10 @@ snapshots: - '@cloudflare/workers-types' - '@opentelemetry/api' - better-call@1.3.2(zod@4.3.6): + better-call@1.3.6(zod@4.3.6): dependencies: - '@better-auth/utils': 0.3.1 - '@better-fetch/fetch': 1.1.21 + '@better-auth/utils': 0.4.2 + '@better-fetch/fetch': 1.3.1 rou3: 0.7.12 set-cookie-parser: 3.1.0 optionalDependencies: @@ -10172,7 +10180,7 @@ snapshots: kleur@4.1.5: {} - kysely@0.28.14: {} + kysely@0.29.2: {} lefthook-darwin-arm64@2.1.4: optional: true From 3c7ddafa05d8f17fb52168bf4638f09251c3d0ff Mon Sep 17 00:00:00 2001 From: Felix Weinberger <3823880+felixweinberger@users.noreply.github.com> Date: Thu, 2 Jul 2026 13:17:08 +0100 Subject: [PATCH 6/6] test(conformance): bump referee to 0.2.0-alpha.9; arm SEP-2575 diagnostic fixtures; fix post-dispatch -32021 HTTP status (#2399) --- .changeset/post-dispatch-32021-http-400.md | 6 ++ docs/migration/support-2026-07-28.md | 12 ++++ .../src/shared/inboundClassification.ts | 23 ++++++-- .../test/shared/errorHttpStatusMatrix.test.ts | 41 +++++++++---- .../shared/inboundLadderCellSheet.test.ts | 45 ++------------- .../server/src/server/createMcpHandler.ts | 8 ++- .../server/src/server/perRequestTransport.ts | 33 ++++++++--- .../test/server/perRequestTransport.test.ts | 38 +++++++++++++ pnpm-lock.yaml | 10 ++-- .../expected-failures.2026-07-28.yaml | 7 ++- test/conformance/expected-failures.yaml | 4 +- test/conformance/package.json | 2 +- test/conformance/src/everythingServer.ts | 57 +++++++++++++++++++ 13 files changed, 211 insertions(+), 75 deletions(-) create mode 100644 .changeset/post-dispatch-32021-http-400.md diff --git a/.changeset/post-dispatch-32021-http-400.md b/.changeset/post-dispatch-32021-http-400.md new file mode 100644 index 0000000000..5f51fe0d62 --- /dev/null +++ b/.changeset/post-dispatch-32021-http-400.md @@ -0,0 +1,6 @@ +--- +'@modelcontextprotocol/server': patch +'@modelcontextprotocol/core-internal': patch +--- + +Return HTTP 400 for a `MissingRequiredClientCapabilityError` (`-32021`) produced after dispatch. The spec mandates `400 Bad Request` for this error with no condition on where it arose, but only the pre-dispatch capability gate honored that; the post-handler emission — the `input_required` gate rejecting an embedded request whose required capability the caller did not declare — surfaced in-band on HTTP 200. The JSON-RPC error body is unchanged, every other error code (including a handler relaying a downstream peer's `-32020`/`-32022`) keeps the origin-keyed in-band behavior, and the mapping only applies while the response is uncommitted: an exchange that already streamed — or one hosted with `responseMode: 'sse'`, which opens its stream at dispatch end — keeps its committed 200 and carries the error in-stream. diff --git a/docs/migration/support-2026-07-28.md b/docs/migration/support-2026-07-28.md index 28e5e5c1ea..a9d603d11a 100644 --- a/docs/migration/support-2026-07-28.md +++ b/docs/migration/support-2026-07-28.md @@ -157,6 +157,18 @@ fetch-shaped handler. > `toNodeHandler(handler)` and add the `@modelcontextprotocol/node` import. > `NodeIncomingMessageLike` / `NodeServerResponseLike` are now exported from > `@modelcontextprotocol/node`, not `@modelcontextprotocol/server`. +> +> Also: a `MissingRequiredClientCapabilityError` (`-32021`) produced **after** dispatch +> — the `input_required` gate refusing an embedded request whose capability the caller +> did not declare — now answers HTTP **400** (earlier alphas surfaced it in-band on +> 200). The spec mandates 400 for this error wherever it arises; the JSON-RPC body is +> unchanged. This applies to a handler-thrown `-32021` too: a proxy relaying a +> downstream server's `-32021` should translate it (its `requiredCapabilities` +> describes the downstream hop's envelope) rather than rethrow the bare error. Every +> other handler-produced code (including a relayed `-32020`/`-32022`) +> keeps the in-band 200, and an exchange whose response stream is already open — the +> handler streamed first, or `responseMode: 'sse'` — keeps its committed 200 and +> carries the error in-stream. ### Server over stdio / long-lived connections: `serveStdio` diff --git a/packages/core-internal/src/shared/inboundClassification.ts b/packages/core-internal/src/shared/inboundClassification.ts index bffaae0645..35f694cae1 100644 --- a/packages/core-internal/src/shared/inboundClassification.ts +++ b/packages/core-internal/src/shared/inboundClassification.ts @@ -371,7 +371,9 @@ export const INBOUND_VALIDATION_LADDER: readonly InboundValidationRungDescriptor * the ladder (or a pre-handler protocol gate) produced. Errors produced by * request handlers — whatever their code — stay in-band on HTTP 200, and are * never mapped to an HTTP status by this table; in particular `-32603` and - * domain-specific codes never become a blanket 500. + * domain-specific codes never become a blanket 500. The single exception is + * `MissingRequiredClientCapability` (-32021) — see + * {@linkcode httpStatusForErrorCode}. * * `-32602` (invalid params) deliberately has NO entry: the only invalid-params * rejection that maps to HTTP 400 is the classifier's own envelope rung @@ -390,11 +392,24 @@ export const LADDER_ERROR_HTTP_STATUS: Readonly> = { /** * The HTTP status to answer a JSON-RPC error with, keyed on the error's * origin. `in-band` errors (anything produced by a request handler) are - * always HTTP 200 — the JSON-RPC error response is the payload, not an HTTP - * failure. `ladder` errors map through {@linkcode LADDER_ERROR_HTTP_STATUS}. + * HTTP 200 — the JSON-RPC error response is the payload, not an HTTP + * failure — with ONE exception: `MissingRequiredClientCapability` (-32021), + * whose 400 the spec mandates on the error itself with no origin condition, + * and which the SDK genuinely produces after dispatch (the `input_required` + * capability gate). A handler relaying some downstream peer's `-32020`/`-32022` + * is NOT that peer's spec error and stays in-band like every other handler + * code. `ladder` errors map through {@linkcode LADDER_ERROR_HTTP_STATUS}. + * + * The per-request transport intentionally does NOT delegate to this function: + * its `?? 400` ladder fallback is only correct for entry-gate codes known to + * the table, and would wrongly map dispatch-window errors outside it (a + * window `-32602` must stay in-band on 200). The transport indexes the table + * directly; keep the two in agreement when editing either. */ export function httpStatusForErrorCode(code: number, origin: 'ladder' | 'in-band'): number { - if (origin === 'in-band') return 200; + if (origin === 'in-band') { + return code === ProtocolErrorCode.MissingRequiredClientCapability ? 400 : 200; + } return LADDER_ERROR_HTTP_STATUS[code] ?? 400; } diff --git a/packages/core-internal/test/shared/errorHttpStatusMatrix.test.ts b/packages/core-internal/test/shared/errorHttpStatusMatrix.test.ts index d053f9eae8..db5711775a 100644 --- a/packages/core-internal/test/shared/errorHttpStatusMatrix.test.ts +++ b/packages/core-internal/test/shared/errorHttpStatusMatrix.test.ts @@ -6,9 +6,13 @@ * * - errors produced by the validation ladder or a pre-handler protocol gate * map through the table (`-32601` → 404; the small mandated 400 set); - * - everything a request handler produces — whatever its code, including - * `-32603`, `-32602` and domain-specific codes — stays in-band on HTTP 200, - * never a blanket 500; + * - everything a request handler produces — including `-32603`, `-32602` and + * domain-specific codes — stays in-band on HTTP 200, never a blanket 500; + * - EXCEPT `-32021` (MissingRequiredClientCapability): the spec mandates its + * 400 per-error with no origin condition, and the `input_required` + * capability gate genuinely emits it after dispatch — so it alone is + * status-mapped wherever it arises. A handler relaying a downstream peer's + * `-32020`/`-32022` is not that peer's spec error and stays in-band; * - `-32602` deliberately has no table entry: the classifier's envelope rung * carries its own HTTP 400 and is the only invalid-params rejection that * maps to 400. @@ -45,7 +49,7 @@ describe('the status matrix — pinned cells', () => { expect(httpStatusForErrorCode(row.code, 'ladder')).toBe(row.status); }); - test('every code stays in-band on HTTP 200 when handler-originated — including internal errors and domain codes', () => { + test('every code except -32021 stays in-band on HTTP 200 when handler-originated — including internal errors and domain codes', () => { const handlerCodes = [ ProtocolErrorCode.InternalError, ProtocolErrorCode.InvalidParams, @@ -61,6 +65,16 @@ describe('the status matrix — pinned cells', () => { } }); + test('-32021 is the single code-keyed exception: its spec-mandated 400 applies wherever it arises', () => { + expect(httpStatusForErrorCode(ProtocolErrorCode.MissingRequiredClientCapability, 'in-band')).toBe(400); + expect(httpStatusForErrorCode(ProtocolErrorCode.MissingRequiredClientCapability, 'ladder')).toBe(400); + // The relay contract for the OTHER two spec-defined HTTP errors is + // origin-keyed: a handler-relayed -32020/-32022 is not this server's + // spec error and stays in-band. + expect(httpStatusForErrorCode(HEADER_MISMATCH_ERROR_CODE, 'in-band')).toBe(200); + expect(httpStatusForErrorCode(ProtocolErrorCode.UnsupportedProtocolVersion, 'in-band')).toBe(200); + }); + test('-32603 never becomes a blanket 500: handler-originated internal errors are in-band', () => { expect(LADDER_ERROR_HTTP_STATUS[ProtocolErrorCode.InternalError]).toBeUndefined(); expect(httpStatusForErrorCode(ProtocolErrorCode.InternalError, 'in-band')).toBe(200); @@ -71,12 +85,19 @@ describe('the status matrix — pinned cells', () => { expect(httpStatusForErrorCode(ProtocolErrorCode.InvalidParams, 'in-band')).toBe(200); }); - test('the table is exactly the mandated set (no silent growth)', () => { - expect( - Object.keys(LADDER_ERROR_HTTP_STATUS) - .map(Number) - .sort((a, b) => a - b) - ).toEqual([-32_700, -32_601, -32_600, -32_022, -32_021, -32_020].sort((a, b) => a - b)); + test('the table is exactly the mandated set, keys and values (no silent growth)', () => { + // The parse-error and invalid-request rows joined the table when the + // status matrix was completed alongside the cache fill / capability + // gate work; they were previously carried only by the classifier's own + // httpStatus on the rejection outcomes (same 400, now table-visible). + expect(LADDER_ERROR_HTTP_STATUS).toEqual({ + [-32_700]: 400, + [-32_601]: 404, + [-32_600]: 400, + [-32_022]: 400, + [-32_021]: 400, + [-32_020]: 400 + }); }); }); diff --git a/packages/core-internal/test/shared/inboundLadderCellSheet.test.ts b/packages/core-internal/test/shared/inboundLadderCellSheet.test.ts index 78613d4940..ace5d9d2c3 100644 --- a/packages/core-internal/test/shared/inboundLadderCellSheet.test.ts +++ b/packages/core-internal/test/shared/inboundLadderCellSheet.test.ts @@ -20,13 +20,7 @@ import { describe, expect, test } from 'vitest'; import type { InboundHttpRequest, InboundLadderRejection } from '../../src/shared/inboundClassification'; -import { - classifyInboundRequest, - httpStatusForErrorCode, - INBOUND_VALIDATION_LADDER, - LADDER_ERROR_HTTP_STATUS, - modernOnlyStrictRejection -} from '../../src/shared/inboundClassification'; +import { classifyInboundRequest, INBOUND_VALIDATION_LADDER, modernOnlyStrictRejection } from '../../src/shared/inboundClassification'; import { CLIENT_CAPABILITIES_META_KEY, CLIENT_INFO_META_KEY, PROTOCOL_VERSION_META_KEY } from '../../src/types/constants'; const MODERN_REVISION = '2026-07-28'; @@ -397,37 +391,6 @@ describe('the validation ladder as data', () => { }); }); -describe('HTTP status mapping for ladder-originated errors (origin-keyed)', () => { - test('the table maps exactly the ladder-originated codes', () => { - // The parse-error and invalid-request rows joined the table when the - // status matrix was completed alongside the cache fill / capability - // gate work; they were previously carried only by the classifier's own - // httpStatus on the rejection outcomes (same 400, now table-visible). - expect(LADDER_ERROR_HTTP_STATUS).toEqual({ - [-32_700]: 400, - [-32_601]: 404, - [-32_600]: 400, - [-32_022]: 400, - [-32_021]: 400, - [-32_020]: 400 - }); - }); - - test('the table never maps invalid params: the classifier envelope short-circuit is the only -32602 -> 400 source', () => { - expect(Object.keys(LADDER_ERROR_HTTP_STATUS)).not.toContain(String(-32_602)); - expect(httpStatusForErrorCode(-32_602, 'in-band')).toBe(200); - }); - - test('handler-originated errors stay in-band on HTTP 200, whatever their code', () => { - for (const code of [-32_603, -32_602, -32_601, -32_022, -32_002, -32_000, 1234]) { - expect(httpStatusForErrorCode(code, 'in-band')).toBe(200); - } - }); - - test('ladder-originated codes map to their HTTP statuses', () => { - expect(httpStatusForErrorCode(-32_601, 'ladder')).toBe(404); - expect(httpStatusForErrorCode(-32_022, 'ladder')).toBe(400); - expect(httpStatusForErrorCode(-32_021, 'ladder')).toBe(400); - expect(httpStatusForErrorCode(-32_020, 'ladder')).toBe(400); - }); -}); +// The error→HTTP-status policy (LADDER_ERROR_HTTP_STATUS / httpStatusForErrorCode) +// is pinned in errorHttpStatusMatrix.test.ts — the single test surface for that +// module. This file pins the classifier's cells only. diff --git a/packages/server/src/server/createMcpHandler.ts b/packages/server/src/server/createMcpHandler.ts index 609772bde5..3f7632d570 100644 --- a/packages/server/src/server/createMcpHandler.ts +++ b/packages/server/src/server/createMcpHandler.ts @@ -669,9 +669,11 @@ export function createMcpHandler(factory: McpServerFactory, options: CreateMcpHa // Pre-dispatch capability gate: a request to a method whose processing // structurally requires a client capability the request's validated // envelope did not declare is refused here, before any instance is - // constructed or dispatched. Answering at the entry pins the - // spec-mandated HTTP 400 for this error; a handler-time emission would - // surface in-band on HTTP 200. + // constructed or dispatched. Answering at the entry short-circuits + // before factory construction and pins the spec-mandated HTTP 400 for + // this error unconditionally; a handler-time -32021 (the + // input_required gate) also maps to 400 at the per-request transport, + // but only while no response has been committed. if (route.messageKind === 'request') { const required = requiredClientCapabilitiesForRequest(route.message.method); if (required !== undefined) { diff --git a/packages/server/src/server/perRequestTransport.ts b/packages/server/src/server/perRequestTransport.ts index c9d24c6f63..4d96bc71c7 100644 --- a/packages/server/src/server/perRequestTransport.ts +++ b/packages/server/src/server/perRequestTransport.ts @@ -27,10 +27,14 @@ * stream, and request-header validation (which belongs to middleware). The * exchange is single-use; serving another request requires a new transport * (and, in the per-request serving model, a fresh server instance). + * + * Consumers wiring this transport into a custom entry (instead of + * `createMcpHandler`) inherit the spec's per-error HTTP mandate with it: a + * terminal `MissingRequiredClientCapabilityError` (-32021) MUST answer 400, + * which this transport applies whenever the response is still uncommitted. */ import type { AuthInfo, - JSONRPCErrorResponse, JSONRPCMessage, JSONRPCNotification, JSONRPCRequest, @@ -45,6 +49,7 @@ import { isJSONRPCRequest, isJSONRPCResultResponse, LADDER_ERROR_HTTP_STATUS, + ProtocolErrorCode, SdkError, SdkErrorCode } from '@modelcontextprotocol/core-internal'; @@ -252,13 +257,27 @@ export class PerRequestHTTPServerTransport implements Transport { // validation ladder, the era registry gate and handoff check, a // missing handler — are answered with the mapped HTTP status from // the ladder table. Handler-produced errors, whatever their code, - // stay in-band on HTTP 200. Ladder rejections keep that mapped - // status in every response mode (the SSE upgrade is deferred to - // the first actual send), so a forced-`sse` exchange still - // answers pre-dispatch rejections as plain HTTP errors. + // stay in-band on HTTP 200 — except + // MissingRequiredClientCapability (-32021), whose 400 the spec + // mandates per-error with no origin condition and whose only + // SDK-produced post-window source (the input_required capability + // gate) cannot fire any earlier — a handler-minted -32021 gets + // the same 400; a handler RELAYING a downstream peer's + // -32020/-32022 is not that peer's spec error and stays in-band. + // Must agree with httpStatusForErrorCode (core-internal), which + // is deliberately NOT called here: its `?? 400` ladder fallback + // would wrongly map window codes outside the table. + // The mapping applies only while no response has been committed: + // once the stream is open — the handler streamed first, or the + // exchange is forced-`sse` (which settles its 200 at dispatch + // end) — the status is on the wire and the error rides the + // stream. Pre-dispatch ladder rejections always precede the + // forced-`sse` upgrade, so they keep their mapped status in every + // response mode. + const errorCode = isJSONRPCErrorResponse(message) ? message.error.code : undefined; const ladderStatus = - this._dispatchWindowOpen && isJSONRPCErrorResponse(message) - ? LADDER_ERROR_HTTP_STATUS[(message as JSONRPCErrorResponse).error.code] + errorCode !== undefined && (this._dispatchWindowOpen || errorCode === ProtocolErrorCode.MissingRequiredClientCapability) + ? LADDER_ERROR_HTTP_STATUS[errorCode] : undefined; if (ladderStatus !== undefined && this._sse === undefined) { this.settleResponse(Response.json(message, { status: ladderStatus, headers: { 'Content-Type': 'application/json' } })); diff --git a/packages/server/test/server/perRequestTransport.test.ts b/packages/server/test/server/perRequestTransport.test.ts index 035e81b83b..364955615a 100644 --- a/packages/server/test/server/perRequestTransport.test.ts +++ b/packages/server/test/server/perRequestTransport.test.ts @@ -199,6 +199,9 @@ describe('HTTP status mapping', () => { }); it('keeps a handler-thrown unsupported-protocol-version error in-band on HTTP 200', async () => { + // A handler relaying a downstream peer's -32022 is not THIS server + // rejecting the caller's version; like the -32601 relay above it must + // not be re-mapped just because the ladder table maps that code. const { server } = modernServer({ toolsCallHandler: async () => { throw new ProtocolError(-32_022, 'Unsupported protocol version: 2099-01-01'); @@ -210,6 +213,41 @@ describe('HTTP status mapping', () => { expect(errorOf(await response.json())?.code).toBe(-32_022); }); + it('maps a post-dispatch -32021 (MissingRequiredClientCapability) to HTTP 400: the spec mandates that status per-error', async () => { + const { server } = modernServer({ + toolsCallHandler: async () => { + throw new ProtocolError(-32_021, 'Missing required client capabilities: sampling', { + requiredCapabilities: { sampling: {} } + }); + } + }); + const transport = await connectedTransport(server); + const response = await transport.handleMessage(toolsCall()); + expect(response.status).toBe(400); + // The spec shape: `requiredCapabilities` is a ClientCapabilities + // OBJECT, never an array. + expect(errorOf(await response.json())).toMatchObject({ code: -32_021, data: { requiredCapabilities: { sampling: {} } } }); + }); + + it('leaves a post-dispatch -32021 on the already-open HTTP 200 stream when the handler streamed first', async () => { + // Once the lazy SSE upgrade has happened, the 200 is committed — and + // the error must still REACH the client as the stream's terminal + // frame rather than being swallowed by the status-mapping arm. + const { server } = modernServer({ + toolsCallHandler: async ctx => { + await ctx.mcpReq.notify({ method: 'notifications/progress', params: { progressToken: 'capability-test', progress: 1 } }); + throw new ProtocolError(-32_021, 'Missing required client capabilities: sampling'); + } + }); + const transport = await connectedTransport(server); + const response = await transport.handleMessage(toolsCall()); + expect(response.status).toBe(200); + expect(response.headers.get('content-type')).toBe('text/event-stream'); + const frames = (await response.text()).split('\n\n').filter(frame => frame.includes('data: ')); + const terminal = frames.at(-1)!; + expect(JSON.parse(terminal.split('data: ')[1]!)).toMatchObject({ id: 1, error: { code: -32_021 } }); + }); + it('keeps handler-produced invalid-params errors in-band on HTTP 200 (never status-mapped)', async () => { const { server } = modernServer({ toolsCallHandler: async () => { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 496cfefe2b..eb4442a9f4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1855,8 +1855,8 @@ importers: specifier: workspace:^ version: link:../../packages/client '@modelcontextprotocol/conformance': - specifier: 0.2.0-alpha.7 - version: 0.2.0-alpha.7(@cfworker/json-schema@4.1.1) + specifier: 0.2.0-alpha.9 + version: 0.2.0-alpha.9(@cfworker/json-schema@4.1.1) '@modelcontextprotocol/core-internal': specifier: workspace:^ version: link:../../packages/core-internal @@ -3140,8 +3140,8 @@ packages: '@manypkg/get-packages@1.1.3': resolution: {integrity: sha512-fo+QhuU3qE/2TQMQmbVMqaQ6EWbMhi4ABWP+O4AM1NqPBuy0OrApV5LO6BrrgnhtAHS2NH6RrVk9OL181tTi8A==} - '@modelcontextprotocol/conformance@0.2.0-alpha.7': - resolution: {integrity: sha512-S3usVyTWdEqvJpyGnXAz6Uj4yboBwT44lrmMqaQtxKcw4PK8H5XfdH5NQqNmDl9/zQbk4oDxtXqzUyGqTbEDzg==} + '@modelcontextprotocol/conformance@0.2.0-alpha.9': + resolution: {integrity: sha512-Bi5P5TQlOQGPJxCT7UAHbpG7wsR7sNZskHGtCoZBo6vDu416D2FXPgM4wKbg91teIgj4HjGkhnzlvP7U2dszfQ==} hasBin: true '@modelcontextprotocol/sdk@1.29.0': @@ -7763,7 +7763,7 @@ snapshots: globby: 11.1.0 read-yaml-file: 1.1.0 - '@modelcontextprotocol/conformance@0.2.0-alpha.7(@cfworker/json-schema@4.1.1)': + '@modelcontextprotocol/conformance@0.2.0-alpha.9(@cfworker/json-schema@4.1.1)': dependencies: '@modelcontextprotocol/sdk': 1.29.0(@cfworker/json-schema@4.1.1)(zod@4.3.6) '@octokit/rest': 22.0.1 diff --git a/test/conformance/expected-failures.2026-07-28.yaml b/test/conformance/expected-failures.2026-07-28.yaml index eebd8e1721..bcc454700b 100644 --- a/test/conformance/expected-failures.2026-07-28.yaml +++ b/test/conformance/expected-failures.2026-07-28.yaml @@ -30,5 +30,8 @@ client: [] server: [] # --- Carried-forward scenarios (also run by the 2025 legs) --- - # (empty: json-schema-2020-12 burned by SEP-2106 fixture; sep-2164-resource-not-found - # burned by the spec#2907 error-code renumber + alpha.5 referee.) + # (empty: json-schema-2020-12 burned by the SEP-2106 fixture; + # sep-2164-resource-not-found burned by the spec#2907 error-code renumber + + # alpha.5 referee; server-stateless burned by conformance#376 at alpha.9 — + # the referee's `requiredCapabilities` assertion now matches the schema's + # `ClientCapabilities` object, which is what this SDK emits.) diff --git a/test/conformance/expected-failures.yaml b/test/conformance/expected-failures.yaml index fe1e950a5d..79e5facbd2 100644 --- a/test/conformance/expected-failures.yaml +++ b/test/conformance/expected-failures.yaml @@ -2,7 +2,7 @@ # CI exits 0 if only these fail, exits 1 on unexpected failures or stale entries. # # Baseline established against the published @modelcontextprotocol/conformance -# release pinned in package.json (0.2.0-alpha.7). Newer conformance releases +# release pinned in package.json (0.2.0-alpha.9). Newer conformance releases # are adopted by deliberately bumping the package.json pin and reconciling # this file in the same change. # @@ -24,7 +24,7 @@ client: [] server: # --- SEP-2663 (io.modelcontextprotocol/tasks) — server SDK does not implement the tasks extension --- - # Extension-tagged scenarios; selected only by `--suite all` (the alpha.7 referee + # Extension-tagged scenarios; selected only by `--suite all` (the alpha.9 referee # has no server-side `--suite extensions`). The active/draft/2026 legs never select # them, so they cannot flag these entries as stale. `tasks-status-notifications` is # intentionally absent: the referee SKIPs it unconditionally (harness rewrite pending diff --git a/test/conformance/package.json b/test/conformance/package.json index f3e8fcd3e4..cf120715df 100644 --- a/test/conformance/package.json +++ b/test/conformance/package.json @@ -38,7 +38,7 @@ "test:conformance:all": "pnpm run test:conformance:client:all && pnpm run test:conformance:server:all" }, "devDependencies": { - "@modelcontextprotocol/conformance": "0.2.0-alpha.7", + "@modelcontextprotocol/conformance": "0.2.0-alpha.9", "@modelcontextprotocol/client": "workspace:^", "@modelcontextprotocol/server": "workspace:^", "@modelcontextprotocol/core-internal": "workspace:^", diff --git a/test/conformance/src/everythingServer.ts b/test/conformance/src/everythingServer.ts index cc75173e10..425b4d6647 100644 --- a/test/conformance/src/everythingServer.ts +++ b/test/conformance/src/everythingServer.ts @@ -186,6 +186,63 @@ function createMcpServer() { }) ); + // SEP-2575 `server-stateless` diagnostic fixtures: the scenario hardcodes + // these three tool names, and at conformance alpha.8 (conformance#372) the + // checks behind them fail as untestable when the names are missing. + + // Requires the `sampling` client capability via an MRTR createMessage + // input request; the scenario calls it with empty clientCapabilities and + // expects -32021 over HTTP 400. + mcpServer.registerTool( + 'test_missing_capability', + { + description: 'SEP-2575: requires the `sampling` client capability (drives the -32021 undeclared-capability rejection)', + inputSchema: z.object({}) + }, + async (_args, ctx): Promise => { + if (ctx.mcpReq.inputResponses?.['llm_answer'] === undefined) { + return inputRequired({ + inputRequests: { + llm_answer: inputRequired.createMessage({ + messages: [{ role: 'user', content: { type: 'text', text: 'Reply with the single word: pong' } }], + maxTokens: 16 + }) + } + }); + } + return { content: [{ type: 'text', text: 'sampling round-trip complete' }] }; + } + ); + + // A plain successful call: the check only asserts that the response stream + // carries no independent top-level JSON-RPC request. It must not elicit + // (the scenario declares no `elicitation` capability); the referee's own + // reference server does not elicit here either. + mcpServer.registerTool( + 'test_streaming_elicitation', + { + description: 'SEP-2575: yields a response stream carrying no independent top-level JSON-RPC requests', + inputSchema: z.object({}) + }, + async (): Promise => ({ + content: [{ type: 'text', text: 'stream observed: result frames only, no top-level requests' }] + }) + ); + + // `ctx.mcpReq.log` is gated on the request's `_meta.logLevel`; the scenario + // omits it and asserts no notifications/message frame appears. + mcpServer.registerTool( + 'test_logging_tool', + { + description: 'SEP-2575: logs via ctx.mcpReq.log so the no-log-without-logLevel rule is exercised', + inputSchema: z.object({}) + }, + async (_args, ctx): Promise => { + await ctx.mcpReq.log('info', 'test_logging_tool ran (delivered only when the request set _meta.logLevel)'); + return { content: [{ type: 'text', text: 'logged through the request-scoped, logLevel-gated channel' }] }; + } + ); + // Simple text tool mcpServer.registerTool( 'test_simple_text',