diff --git a/.claude/skills/mcp-sdk-tier-audit/SKILL.md b/.claude/skills/mcp-sdk-tier-audit/SKILL.md index 509df1d8..1439e70e 100644 --- a/.claude/skills/mcp-sdk-tier-audit/SKILL.md +++ b/.claude/skills/mcp-sdk-tier-audit/SKILL.md @@ -81,7 +81,8 @@ npm run --silent tier-check -- \ --output json ``` -Omit `--requirements` only if the user did not name a revision. When it is set the +Omit `--requirements` only if the user did not name a revision (the CLI then +scores against the latest shipped revision's requirement set alone). When it is set the scorecard reports `requirements_revision`, both pass rates count exactly the scenarios that revision requires, and anything run but not scored carries a `notScoredReason` of `extension` or `added-after-release`. `requirements_revisions` @@ -93,7 +94,7 @@ If no client-cmd was detected, omit the `--client-cmd` flag (client conformance The CLI output includes server conformance pass rate, client conformance pass rate (with per-spec-version breakdown), issue triage compliance, P0 resolution times, label taxonomy, stable release status, policy signal files, and spec tracking gap. Parse the JSON output to feed into Step 4. -The conformance results now include a `specVersions` field on each detail entry, enabling per-version pass rate analysis. The `list` command also shows spec version tags: `node dist/index.js list` shows `[2025-06-18]`, `[2025-11-25]`, `[draft]`, or `[extension]` next to each scenario. +The conformance results now include a `specVersions` field on each detail entry, enabling per-version pass rate analysis. The `list` command also shows spec version tags: `node dist/index.js list` shows the dated revisions a scenario applies to (`[2025-06-18,2025-11-25,2026-07-28]`), `[draft]` for a scenario targeting the unreleased revision after the latest, or `[extension]`. ### Conformance Baseline Check @@ -188,7 +189,7 @@ Informational (not scored for tier): | ------------ | ----- | --------- | | Client: Auth | 0/1 | 0/2 | -The tier-scoring table only includes date-versioned scenarios. `draft` and `extension` scenarios are shown separately as informational — they do not affect tier advancement. +The tier-scoring table only includes date-versioned scenarios (`2026-07-28` is a dated release and scores). `draft` (requirements newer than the latest release) and `extension` scenarios are shown separately as informational — they do not affect tier advancement. This immediately shows where failures concentrate. Failures clustered in Client: Auth / `2025-11-25` means "new auth features not yet implemented" — a scope gap, not a quality problem. Failures in Server or Client: Core are more concerning. diff --git a/.claude/skills/mcp-sdk-tier-audit/references/tier-requirements.md b/.claude/skills/mcp-sdk-tier-audit/references/tier-requirements.md index 077917b2..e5d83c66 100644 --- a/.claude/skills/mcp-sdk-tier-audit/references/tier-requirements.md +++ b/.claude/skills/mcp-sdk-tier-audit/references/tier-requirements.md @@ -34,9 +34,9 @@ Source: `modelcontextprotocol/docs/community/sdk-tiers.mdx` in the spec reposito Every scenario in the conformance suite has a `specVersions` field indicating which spec version it targets. The valid values are defined as the `SpecVersion` type (as a list) in `src/types.ts` — run `node dist/index.js list` to see the current mapping of scenarios to spec versions. -Date-versioned scenarios (e.g. `2025-06-18`, `2025-11-25`) count toward tier scoring. `draft` and `extension` scenarios are listed separately as informational. +Date-versioned scenarios (e.g. `2025-06-18`, `2025-11-25`, `2026-07-28`) count toward tier scoring. `draft` (requirements newer than the latest dated release) and `extension` scenarios are listed separately as informational. -The `--spec-version` CLI flag filters scenarios cumulatively for date versions (e.g. `--spec-version 2025-06-18` includes `2025-03-26` + `2025-06-18`). For `draft`/`extension`, it returns exact matches only. +The `--spec-version` CLI flag selects the scenarios applicable at a revision: cumulative for date versions (e.g. `--spec-version 2025-06-18` includes `2025-03-26` + `2025-06-18`) minus anything that revision removed (`2026-07-28` drops the stateful-lifecycle scenarios); `draft` adds any draft-only scenarios on top of the latest release. Extension scenarios are never selected by `--spec-version` (use `--suite extensions`). The tier-check output includes a per-version pass rate breakdown alongside the aggregate. diff --git a/AGENTS.md b/AGENTS.md index ca88ff87..d06af6cd 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -46,6 +46,7 @@ Keep scenarios separate when they're genuinely independent features or when they - Start with **one end-to-end scenario** covering the happy path with many checks along the way. - Don't add "step 1 only" and "step 1+2" as separate scenarios — the second subsumes the first. - Register the scenario in the appropriate suite list in `src/scenarios/index.ts` (`core`, `extensions`, `backcompat`, etc.). +- Set `source.introducedIn` to the dated revision the requirement shipped in (e.g. `'2026-07-28'`). Use `DRAFT_SPEC_VERSION` only for a requirement that is in `docs/specification/draft/` but not yet in a dated release; such scenarios run under `--suite draft` / `--spec-version draft` and stay out of the default suite and tier scoring until the draft ships and they are retagged to its date. `DRAFT_PROTOCOL_VERSION` is the draft's wire string, not a tag. ## Check conventions diff --git a/README.md b/README.md index d5c4cb3f..fc866566 100644 --- a/README.md +++ b/README.md @@ -63,15 +63,15 @@ npx @modelcontextprotocol/conformance client --command "" --scen - `--command` - The command to run your MCP client (can include flags) - `--scenario` - The test scenario to run (e.g., "initialize") -- `--suite` - Run a suite of tests in parallel: `all`, `core`, `extensions`, `backcompat`, `auth`, `metadata`, `draft` (scenarios targeting the in-progress draft spec), or `sep-835` -- `--spec-version ` - Filter scenarios by spec version (e.g., `2025-11-25`, `2026-07-28`; `draft` is accepted as an alias for the current draft identifier). The draft version selects the latest dated release plus any draft-only scenarios. When omitted, the version is inferred from the scenario's spec applicability (draft-only scenarios run at the draft version, everything else at the latest dated release); an explicitly requested version outside a scenario's applicability window skips the scenario (exit 0) unless `--force` is passed +- `--suite` - Run a suite of tests in parallel: `all`, `core`, `extensions`, `backcompat`, `auth`, `metadata`, `draft` (scenarios for requirements newer than the latest dated release — currently empty, since everything that was in it shipped in `2026-07-28`), or `sep-835` +- `--spec-version ` - Target a spec revision: a dated release (`2025-03-26`, `2025-06-18`, `2025-11-25`, `2026-07-28`) or `draft` for the unreleased revision after the latest. A dated version selects every scenario applicable at that revision (cumulative, minus anything the revision removed); `draft` selects the latest release's set plus any draft-only scenarios and runs them at the draft's wire `protocolVersion` (currently also `2026-07-28`, until the spec repo gives the next draft its own). When omitted, each scenario runs at the latest dated release if it applies there, otherwise at the newest revision it does apply to (draft-only scenarios at the draft, scenarios removed in `2026-07-28` at `2025-11-25`); an explicitly requested version outside a scenario's applicability window skips the scenario (exit 0) unless `--force` is passed - `--force` - Run a scenario even if it is not applicable at the requested `--spec-version` - `--requirements ` - Run exactly what a spec revision requires, frozen at its release (see [Conformance Requirements](#conformance-requirements)) - `--expected-failures ` - Path to YAML baseline file of known failures (see [Expected Failures](#expected-failures)) - `--timeout` - Timeout in milliseconds (default: 30000) - `--verbose` - Show verbose output -The framework appends `` as an argument to your command and sets the `MCP_CONFORMANCE_SCENARIO` environment variable to the scenario name. For scenarios that require additional context (e.g., client credentials), the `MCP_CONFORMANCE_CONTEXT` environment variable contains a JSON object with scenario-specific data. When `--spec-version` is passed, its resolved value is forwarded to the client process as `MCP_CONFORMANCE_PROTOCOL_VERSION`; example clients can use this value directly as their `protocolVersion`. SDKs that hard-code their protocol version can ignore it. Clients under test must derive the lifecycle from the protocol version they are asked to run: dated versions through `2025-11-25` use the stateful lifecycle (initialize handshake), while the 2026 draft (`2026-07-28`) uses the stateless lifecycle (per-request `_meta`). +The framework appends `` as an argument to your command and sets the `MCP_CONFORMANCE_SCENARIO` environment variable to the scenario name. For scenarios that require additional context (e.g., client credentials), the `MCP_CONFORMANCE_CONTEXT` environment variable contains a JSON object with scenario-specific data. The wire `protocolVersion` of the revision a scenario runs at is forwarded to the client process as `MCP_CONFORMANCE_PROTOCOL_VERSION`; example clients can use this value directly as their `protocolVersion`. SDKs that hard-code their protocol version can ignore it. Clients under test must derive the lifecycle from the protocol version they are asked to run: dated versions through `2025-11-25` use the stateful lifecycle (initialize handshake), while `2026-07-28` and later use the stateless lifecycle (per-request `_meta`). ### Server Testing @@ -83,7 +83,8 @@ npx @modelcontextprotocol/conformance server --url [--scenario ] - `--url` - URL of the server to test - `--scenario ` - Test scenario to run (e.g., "server-initialize"). Runs all available scenarios by default -- `--suite ` - Suite to run: "active" (default; excludes pending and draft-spec scenarios), "all", "draft" (scenarios targeting the in-progress draft spec), or "pending" +- `--suite ` - Suite to run: "active" (default; excludes pending and draft-only scenarios), "all", "draft" (scenarios for requirements newer than the latest dated release — currently empty), or "pending" +- `--spec-version ` - Target a spec revision (see Client Testing above). When omitted, scenarios that apply to `2026-07-28` run at that (stateless) wire and scenarios it removed run at `2025-11-25`; pass `--spec-version 2025-11-25` to test a server that only implements the stateful lifecycle - `--requirements ` - Run exactly what a spec revision requires, frozen at its release (see [Conformance Requirements](#conformance-requirements)) - `--expected-failures ` - Path to YAML baseline file of known failures (see [Expected Failures](#expected-failures)) - `--verbose` - Show verbose output @@ -364,7 +365,7 @@ npm start -- sdk typescript-sdk --mode client --suite auth # Target a specific spec version (passed through to the underlying run). # When omitted, the SDK's `specVersion` from KNOWN_SDKS is used, if set — # e.g. typescript-sdk-v1 defaults to 2025-11-25. -npm start -- sdk typescript-sdk --mode client --spec-version draft +npm start -- sdk typescript-sdk --mode client --spec-version 2026-07-28 ``` Build/run commands for each official SDK are looked up by name from [`src/sdk-runner/known-sdks.ts`](src/sdk-runner/known-sdks.ts) — no config file is required in the SDK repo. Resolution order is **CLI flag > built-in entry**, so any field can be overridden on the command line for refs that diverge from the built-in. @@ -434,8 +435,11 @@ npx @modelcontextprotocol/conformance tier-check \ ``` Omit `--client-cmd` and the client leg is skipped and reported as a gap. Omit -`--requirements` and scoring falls back to the suite as it stands today, which is -not what you want for a tier claim; see [Conformance Requirements](#conformance-requirements). +`--requirements` (and `--spec-version`) and scoring uses the latest shipped +revision's requirement set alone (`2026-07-28`), which claims one revision, not +every one; pass `--requirements` explicitly for a tier claim. `--spec-version` +without `--requirements` scores the suite as it stands today at that revision, +which is not frozen; see [Conformance Requirements](#conformance-requirements). The exit code is a CI verdict on the machine-checkable half: nonzero when any scored conformance scenario fails or a leg could not be measured. Governance diff --git a/examples/clients/typescript/everything-client.ts b/examples/clients/typescript/everything-client.ts index b3ccbed3..88165887 100644 --- a/examples/clients/typescript/everything-client.ts +++ b/examples/clients/typescript/everything-client.ts @@ -28,8 +28,8 @@ import type { import { JWT_BEARER_GRANT_TYPE } from '../../../src/scenarios/client/auth/helpers/createWorkloadJwt.js'; import { ElicitRequestSchema } from '@modelcontextprotocol/sdk/types.js'; import { ClientConformanceContextSchema } from '../../../src/schemas/context.js'; -import { DRAFT_PROTOCOL_VERSION } from '../../../src/types.js'; -import { STATELESS_SPEC_VERSIONS } from '../../../src/connection/select.js'; +import { LATEST_SPEC_VERSION } from '../../../src/types.js'; +import { STATELESS_PROTOCOL_VERSIONS } from '../../../src/connection/select.js'; import { auth, extractWWWAuthenticateParams @@ -94,13 +94,14 @@ const PROTOCOL_VERSION = process.env.MCP_CONFORMANCE_PROTOCOL_VERSION; // in-repo client imports the stateless version set from src/ so it cannot // drift from the runner's mapping. const USE_STATELESS_LIFECYCLE = PROTOCOL_VERSION - ? (STATELESS_SPEC_VERSIONS as readonly string[]).includes(PROTOCOL_VERSION) + ? STATELESS_PROTOCOL_VERSIONS.includes(PROTOCOL_VERSION) : false; // Wire protocolVersion for stateless requests: the runner-resolved version -// when available (so a dated stateless release is exercised under its own -// identifier), the current draft otherwise. -const STATELESS_PROTOCOL_VERSION = PROTOCOL_VERSION ?? DRAFT_PROTOCOL_VERSION; +// when available (so each stateless revision is exercised under its own +// identifier), the latest release otherwise. +const STATELESS_PROTOCOL_VERSION: string = + PROTOCOL_VERSION ?? LATEST_SPEC_VERSION; const STATELESS_META_BASE = { 'io.modelcontextprotocol/clientInfo': { @@ -336,7 +337,7 @@ async function runRequestMetadataClient(serverUrl: string): Promise { const clone = response.clone(); try { const errorResult = await clone.json(); - // UnsupportedProtocolVersionError is -32022 in the draft schema. + // UnsupportedProtocolVersionError is -32022 (2026-07-28 onward). if (errorResult.error?.code === -32022) { logger.debug( 'Received UnsupportedProtocolVersionError, starting negotiation...' @@ -344,7 +345,10 @@ async function runRequestMetadataClient(serverUrl: string): Promise { const serverSupported: string[] = errorResult.error.data?.supported || []; const clientSupported = [ - ...new Set([STATELESS_PROTOCOL_VERSION, DRAFT_PROTOCOL_VERSION]) + ...new Set([ + STATELESS_PROTOCOL_VERSION, + ...STATELESS_PROTOCOL_VERSIONS + ]) ]; const mutuallySupported = clientSupported.filter((v) => serverSupported.includes(v) @@ -459,7 +463,7 @@ registerScenarios( 'auth/token-endpoint-auth-none', // Resource mismatch (client should error when PRM resource doesn't match) 'auth/resource-mismatch', - // SEP-2207: Offline access / refresh token guidance (draft) + // SEP-2207: Offline access / refresh token guidance (2026-07-28) 'auth/offline-access-scope', 'auth/offline-access-not-supported', // SEP-2468: ISS parameter - positive scenarios (standard client is fine) diff --git a/examples/servers/typescript/everything-server.ts b/examples/servers/typescript/everything-server.ts index c76c1f56..697619f0 100644 --- a/examples/servers/typescript/everything-server.ts +++ b/examples/servers/typescript/everything-server.ts @@ -1235,7 +1235,7 @@ const LEGACY_SESSION_PROTOCOL_VERSIONS = [ '2025-11-25' ]; -// Stateless (draft) operations whose results MUST carry the SEP-2549 caching +// Stateless (2026-07-28+) operations whose results MUST carry the SEP-2549 caching // hints (`ttlMs`, `cacheScope`). const STATELESS_CACHEABLE_METHODS: ReadonlySet = new Set([ 'server/discover', @@ -1246,9 +1246,9 @@ const STATELESS_CACHEABLE_METHODS: ReadonlySet = new Set([ 'resources/read' ]); -/** Normalize a stateless (draft) JSON-RPC response. Draft results MUST carry `resultType` +/** Normalize a stateless (2026-07-28+) JSON-RPC response. These results MUST carry `resultType` * and cacheable operations the SEP-2549 caching hints; stamp any the dispatch site did - * not set so every stateless result is draft-schema-valid. Errors pass through untouched. */ + * not set so every stateless result is schema-valid. Errors pass through untouched. */ function normalizeStatelessResponse( method: string, payload: { result?: Record; [key: string]: unknown } @@ -1620,7 +1620,8 @@ app.post('/mcp', async (req, res) => { { uri: 'test://stateless-static-text', name: 'Stateless Static Text', - description: 'A static text resource served on the draft path', + description: + 'A static text resource served on the stateless path', mimeType: 'text/plain' } ], @@ -1677,7 +1678,7 @@ app.post('/mcp', async (req, res) => { { uri, mimeType: 'text/plain', - text: 'Static text content from the stateless draft path.' + text: 'Static text content from the stateless path.' } ], ttlMs: 300000, diff --git a/scripts/sync-schema.ts b/scripts/sync-schema.ts index a5d41a0b..ed66abe5 100644 --- a/scripts/sync-schema.ts +++ b/scripts/sync-schema.ts @@ -2,15 +2,24 @@ /** * Vendor schema/{version}/schema.ts and schema/{version}/schema.json from the * modelcontextprotocol spec repo into src/spec-types/{version}.ts and - * src/spec-types/{version}.schema.json at a pinned SHA. + * src/spec-types/{version}.schema.json at a pinned SHA, for every entry of + * DATED_SPEC_VERSIONS plus `draft`, and regenerate src/spec-types/schemas.ts + * (the version -> JSON Schema map the wire validator compiles). + * + * Publishing a new spec revision: append its date to DATED_SPEC_VERSIONS in + * src/types.ts, then run this at the release tag. Nothing else in the schema + * plumbing needs editing. * * Usage: npm run sync-schema -- */ import { execFileSync } from 'node:child_process'; import { mkdirSync, writeFileSync, rmSync, copyFileSync } from 'node:fs'; import { join } from 'node:path'; +import { DATED_SPEC_VERSIONS, DRAFT_SPEC_VERSION } from '../src/types'; -const VERSIONS = ['2025-03-26', '2025-06-18', '2025-11-25', 'draft'] as const; +// DRAFT_SPEC_VERSION is the literal 'draft', which is also the spec repo's +// directory name for the in-progress revision. +const VERSIONS = [...DATED_SPEC_VERSIONS, DRAFT_SPEC_VERSION] as const; const SPEC_REPO = 'https://github.com/modelcontextprotocol/modelcontextprotocol.git'; const OUT_DIR = join(process.cwd(), 'src', 'spec-types'); @@ -29,6 +38,13 @@ mkdirSync(OUT_DIR, { recursive: true }); const git = (args: string[]) => execFileSync('git', args, { cwd: tmp, encoding: 'utf8' }); +/** `2025-03-26` -> `schema2025_03_26`, `draft` -> `schemaDraft`. */ +function importName(v: string): string { + return v === DRAFT_SPEC_VERSION + ? 'schemaDraft' + : `schema${v.replace(/-/g, '_')}`; +} + try { console.log(`Fetching ${SPEC_REPO} @ ${ref} ...`); git(['init', '-q']); @@ -47,6 +63,20 @@ try { console.log(` ${v} -> src/spec-types/${v}.schema.json`); } + const schemasTs = + `// Generated by scripts/sync-schema.ts from DATED_SPEC_VERSIONS + draft.\n` + + `// Do not edit: add the version to src/types.ts and re-run the script.\n` + + `import type { SpecVersion } from '../types';\n` + + VERSIONS.map( + (v) => `import ${importName(v)} from './${v}.schema.json';\n` + ).join('') + + `\n/** Vendored JSON Schema for every spec version the suite can target. */\n` + + `export const SPEC_SCHEMAS: Record> = {\n` + + VERSIONS.map((v) => ` '${v}': ${importName(v)},\n`).join('') + + `};\n`; + writeFileSync(join(OUT_DIR, 'schemas.ts'), schemasTs, 'utf8'); + console.log(' version map -> src/spec-types/schemas.ts'); + writeFileSync( join(OUT_DIR, 'SOURCE'), `modelcontextprotocol@${sha}\n`, diff --git a/src/connection/connection.test.ts b/src/connection/connection.test.ts index fa3a17dd..3e39b2f3 100644 --- a/src/connection/connection.test.ts +++ b/src/connection/connection.test.ts @@ -1,8 +1,12 @@ import { describe, it, expect, vi, afterEach } from 'vitest'; -import { isStatefulVersion, STATELESS_SPEC_VERSIONS } from './select'; +import { + isStatefulVersion, + STATELESS_PROTOCOL_VERSIONS, + STATELESS_SPEC_VERSIONS +} from './select'; import { connectStateless } from './stateless'; import { JsonRpcError } from './index'; -import { DRAFT_PROTOCOL_VERSION } from '../types'; +import { DRAFT_PROTOCOL_VERSION, DRAFT_SPEC_VERSION } from '../types'; import { takeWireViolations } from '../validation/wire-schema'; describe('STATELESS_SPEC_VERSIONS', () => { @@ -12,8 +16,28 @@ describe('STATELESS_SPEC_VERSIONS', () => { expect(isStatefulVersion(v)).toBe(false); } }); - it('currently contains only the draft version', () => { - expect(STATELESS_SPEC_VERSIONS).toEqual([DRAFT_PROTOCOL_VERSION]); + it('holds every revision from 2026-07-28 on, ending with the draft', () => { + expect(STATELESS_SPEC_VERSIONS[0]).toBe('2026-07-28'); + expect(STATELESS_SPEC_VERSIONS[STATELESS_SPEC_VERSIONS.length - 1]).toBe( + DRAFT_SPEC_VERSION + ); + for (const stateful of [ + '2025-03-26', + '2025-06-18', + '2025-11-25' + ] as const) { + expect(STATELESS_SPEC_VERSIONS).not.toContain(stateful); + } + }); + it('maps to deduplicated wire versions', () => { + // The draft shares 2026-07-28's wire string until the spec repo gives the + // next draft its own marker; either way every entry is a wire string. + expect(STATELESS_PROTOCOL_VERSIONS).toContain('2026-07-28'); + expect(STATELESS_PROTOCOL_VERSIONS).toContain(DRAFT_PROTOCOL_VERSION); + expect(new Set(STATELESS_PROTOCOL_VERSIONS).size).toBe( + STATELESS_PROTOCOL_VERSIONS.length + ); + expect(STATELESS_PROTOCOL_VERSIONS).not.toContain(DRAFT_SPEC_VERSION); }); }); diff --git a/src/connection/select.ts b/src/connection/select.ts index 46679005..ec0c9415 100644 --- a/src/connection/select.ts +++ b/src/connection/select.ts @@ -1,6 +1,6 @@ import { - DATED_SPEC_VERSIONS, - DRAFT_PROTOCOL_VERSION, + SPEC_VERSION_TIMELINE, + protocolVersionFor, type SpecVersion } from '../types'; import type { Connection, ConnectOptions, RunContext } from './index'; @@ -20,12 +20,6 @@ const STATEFUL_VERSIONS: ReadonlySet = new Set([ '2025-11-25' ]); -/** Every spec version the suite can target, in timeline order. */ -const ALL_SPEC_VERSIONS: readonly SpecVersion[] = [ - ...DATED_SPEC_VERSIONS, - DRAFT_PROTOCOL_VERSION -]; - export function isStatefulVersion(v: SpecVersion): boolean { return STATEFUL_VERSIONS.has(v); } @@ -33,12 +27,22 @@ export function isStatefulVersion(v: SpecVersion): boolean { /** * Spec versions that use the stateless lifecycle, derived from * {@link isStatefulVersion} so there is a single source of truth for the - * version→lifecycle mapping. The list grows automatically when the draft is - * dated (added to `DATED_SPEC_VERSIONS` without joining `STATEFUL_VERSIONS`) - * or a second stateless version appears. + * version→lifecycle mapping. The list grows automatically when a new dated + * revision is added to `DATED_SPEC_VERSIONS` without joining + * `STATEFUL_VERSIONS`. */ export const STATELESS_SPEC_VERSIONS: readonly SpecVersion[] = - ALL_SPEC_VERSIONS.filter((v) => !isStatefulVersion(v)); + SPEC_VERSION_TIMELINE.filter((v) => !isStatefulVersion(v)); + +/** + * Wire `protocolVersion` strings of the stateless revisions, deduplicated + * (the draft shares the latest release's wire string until the spec repo + * gives the next draft its own). What a stateless mock server advertises in + * `server/discover` when the run does not pin one version. + */ +export const STATELESS_PROTOCOL_VERSIONS: readonly string[] = [ + ...new Set(STATELESS_SPEC_VERSIONS.map(protocolVersionFor)) +]; export function connectFor( specVersion: SpecVersion diff --git a/src/connection/stateless.test.ts b/src/connection/stateless.test.ts index 79436967..ea5680ec 100644 --- a/src/connection/stateless.test.ts +++ b/src/connection/stateless.test.ts @@ -12,14 +12,18 @@ import { CONFORMANCE_CLIENT_INFO, DEFAULT_CLIENT_CAPABILITIES } from './stateless'; -import { DRAFT_PROTOCOL_VERSION } from '../types'; +import { + DRAFT_PROTOCOL_VERSION, + DRAFT_SPEC_VERSION, + LATEST_SPEC_VERSION +} from '../types'; import { takeWireViolations } from '../validation/wire-schema'; describe('buildStandardHeaders', () => { - test('sets the standard headers pinned to the draft protocol version', () => { + test('sets the standard headers pinned to the latest release', () => { const headers = buildStandardHeaders('tools/list'); expect(headers['Mcp-Method']).toBe('tools/list'); - expect(headers['MCP-Protocol-Version']).toBe(DRAFT_PROTOCOL_VERSION); + expect(headers['MCP-Protocol-Version']).toBe(LATEST_SPEC_VERSION); expect(headers['Content-Type']).toBe('application/json'); expect(headers.Accept).toContain('application/json'); expect(headers.Accept).toContain('text/event-stream'); @@ -51,7 +55,7 @@ describe('withRequestMeta', () => { const params = withRequestMeta({ name: 'echo' }); const meta = params._meta as Record; expect(meta['io.modelcontextprotocol/protocolVersion']).toBe( - DRAFT_PROTOCOL_VERSION + LATEST_SPEC_VERSION ); expect(meta['io.modelcontextprotocol/clientInfo']).toEqual( CONFORMANCE_CLIENT_INFO @@ -126,9 +130,17 @@ describe('spec version plumbing', () => { expect(headers['MCP-Protocol-Version']).toBe('2025-11-25'); }); - test('buildStandardHeaders defaults to the draft version', () => { + test('buildStandardHeaders defaults to the latest release', () => { const headers = buildStandardHeaders('tools/list'); + expect(headers['MCP-Protocol-Version']).toBe(LATEST_SPEC_VERSION); + }); + + test('buildStandardHeaders sends the draft wire version, not the word draft', () => { + const headers = buildStandardHeaders('tools/list', undefined, { + specVersion: DRAFT_SPEC_VERSION + }); expect(headers['MCP-Protocol-Version']).toBe(DRAFT_PROTOCOL_VERSION); + expect(headers['MCP-Protocol-Version']).not.toBe(DRAFT_SPEC_VERSION); }); test('withRequestMeta declares the requested spec version in _meta', () => { @@ -137,9 +149,17 @@ describe('spec version plumbing', () => { expect(meta['io.modelcontextprotocol/protocolVersion']).toBe('2025-11-25'); }); - test('withRequestMeta defaults to the draft version', () => { + test('withRequestMeta defaults to the latest release', () => { const params = withRequestMeta({}); const meta = params._meta as Record; + expect(meta['io.modelcontextprotocol/protocolVersion']).toBe( + LATEST_SPEC_VERSION + ); + }); + + test('withRequestMeta declares the draft wire version for a draft run', () => { + const params = withRequestMeta({}, DRAFT_SPEC_VERSION); + const meta = params._meta as Record; expect(meta['io.modelcontextprotocol/protocolVersion']).toBe( DRAFT_PROTOCOL_VERSION ); diff --git a/src/connection/stateless.ts b/src/connection/stateless.ts index 38890ac8..278913be 100644 --- a/src/connection/stateless.ts +++ b/src/connection/stateless.ts @@ -1,10 +1,10 @@ /** - * Stateless connection: 2026-x lifecycle (SEP-2575). + * Stateless connection: the 2026-07-28-and-later lifecycle (SEP-2575). * * No handshake. Every request carries `_meta` with protocolVersion, clientInfo, * and clientCapabilities, plus the standard headers (MCP-Protocol-Version, * Mcp-Method, Mcp-Name per SEP-2243). Implemented with raw fetch so the - * conformance suite can test draft spec versions before the SDK supports them. + * conformance suite can test spec versions before the SDK supports them. * * Exports two layers: * - `sendStatelessRequest()` — low-level: returns `{status, headers, body, @@ -12,7 +12,8 @@ * HTTP status or error codes use this directly. * - `connectStateless()` — high-level: a `Connection` whose `request()` calls * `sendStatelessRequest()` and throws `JsonRpcError` on error responses. - * The runner picks this via `connectFor()` for `--spec-version draft`. + * The runner picks this via `connectFor()` for stateless spec versions + * (2026-07-28 onward, and the draft). * * Both build their requests through `buildStandardHeaders()` and * `withRequestMeta()` so a strictly-conformant server never rejects harness @@ -20,7 +21,11 @@ * (issues #311, #312, #315). */ -import { DRAFT_PROTOCOL_VERSION, type SpecVersion } from '../types'; +import { + LATEST_SPEC_VERSION, + protocolVersionFor, + type SpecVersion +} from '../types'; import type { JSONRPCNotification } from '../spec-types/2025-11-25'; import { validateWireMessage } from '../validation/wire-schema'; import { JsonRpcError, type Connection, type ConnectOptions } from './index'; @@ -88,7 +93,8 @@ export function mcpNameForRequest( * Accept (both content types), MCP-Protocol-Version, Mcp-Method and (when the * method carries one) Mcp-Name. `options.headers` overrides or extends the * defaults, replacing any default whose name matches case-insensitively. - * `options.specVersion` sets the MCP-Protocol-Version header (default: draft), + * `options.specVersion` sets the MCP-Protocol-Version header (default: the + * latest release), * so scenarios can send the spec version the run was invoked with. */ export function buildStandardHeaders( @@ -99,7 +105,9 @@ export function buildStandardHeaders( const headers: Record = { 'Content-Type': 'application/json', Accept: 'application/json, text/event-stream', - 'MCP-Protocol-Version': options.specVersion ?? DRAFT_PROTOCOL_VERSION, + 'MCP-Protocol-Version': protocolVersionFor( + options.specVersion ?? LATEST_SPEC_VERSION + ), 'Mcp-Method': method }; const name = mcpNameForRequest(method, params); @@ -125,16 +133,17 @@ export function buildStandardHeaders( /** * Merge params with the conformant `_meta` required on every stateless * request. Keys already present in `params._meta` win over the defaults. - * `specVersion` sets the declared protocolVersion (default: draft). + * `specVersion` sets the declared protocolVersion (default: the latest release). */ export function withRequestMeta( params?: Record, - specVersion: SpecVersion = DRAFT_PROTOCOL_VERSION + specVersion: SpecVersion = LATEST_SPEC_VERSION ): Record { return { ...params, _meta: { - 'io.modelcontextprotocol/protocolVersion': specVersion, + 'io.modelcontextprotocol/protocolVersion': + protocolVersionFor(specVersion), 'io.modelcontextprotocol/clientInfo': CONFORMANCE_CLIENT_INFO, 'io.modelcontextprotocol/clientCapabilities': DEFAULT_CLIENT_CAPABILITIES, ...(params?._meta as Record | undefined) @@ -250,7 +259,7 @@ export async function sendStatelessRequest( } = {} ): Promise { const id = nextRequestId++; - const specVersion = options.specVersion ?? DRAFT_PROTOCOL_VERSION; + const specVersion = options.specVersion ?? LATEST_SPEC_VERSION; const headers = buildStandardHeaders(method, params, { headers: options.headers, specVersion: options.specVersion @@ -359,7 +368,7 @@ export async function sendStatelessRequest( */ export async function connectStateless( serverUrl: string, - specVersion: SpecVersion = DRAFT_PROTOCOL_VERSION, + specVersion: SpecVersion = LATEST_SPEC_VERSION, opts: ConnectOptions = {} ): Promise { const notifications: JSONRPCNotification[] = []; diff --git a/src/connection/testing.ts b/src/connection/testing.ts index f9177d19..543e9306 100644 --- a/src/connection/testing.ts +++ b/src/connection/testing.ts @@ -1,15 +1,17 @@ -import { LATEST_SPEC_VERSION, type SpecVersion } from '../types'; +import type { SpecVersion } from '../types'; +import { DEFAULT_TEST_SPEC_VERSION } from '../mock-server/testing'; import { connectFor } from './select'; import type { ConnectOptions, RunContext } from './index'; /** * Build a RunContext for unit tests that drive a scenario directly. - * Defaults to the latest dated spec version (stateful lifecycle) so existing - * tests keep their pre-RunContext behaviour. + * Defaults to {@link DEFAULT_TEST_SPEC_VERSION} (the last stateful revision) + * so tests that stand up an SDK-based fixture server keep working; pass + * `'2026-07-28'` (or the draft) explicitly to exercise the stateless path. */ export function testContext( serverUrl: string, - specVersion: SpecVersion = LATEST_SPEC_VERSION + specVersion: SpecVersion = DEFAULT_TEST_SPEC_VERSION ): RunContext { return { serverUrl, diff --git a/src/index.ts b/src/index.ts index c42be2e3..01a0b841 100644 --- a/src/index.ts +++ b/src/index.ts @@ -36,7 +36,11 @@ import { resolveSpecVersion } from './scenarios'; import type { SpecVersion } from './scenarios'; -import { ConformanceCheck } from './types'; +import { + ConformanceCheck, + DATED_SPEC_VERSIONS, + LATEST_SPEC_VERSION +} from './types'; import { AuthorizationServerOptionsSchema, ClientOptionsSchema, @@ -83,7 +87,7 @@ function resolveRequirements( // failure this flag exists to prevent. if (revision === '') { console.error( - '--requirements needs a revision, such as 2026-07-28. Run `conformance list` to see which are available.' + `--requirements needs a revision, such as ${LATEST_SPEC_VERSION}. Run \`conformance list\` to see which are available.` ); process.exit(1); } @@ -186,6 +190,25 @@ function requiredScenariosOrExit( } } +const SPEC_VERSION_HELP = + `Target a spec revision: a dated release (${DATED_SPEC_VERSIONS.join(', ')}) ` + + 'or "draft" for the unreleased revision after the latest. Selects the scenarios ' + + 'applicable at that revision and the wire version they run at'; + +/** + * The draft suites hold scenarios for requirements newer than the latest + * release. Right after a release they are empty (everything that was "draft" + * shipped and got retagged), which is worth one line so a CI job that still + * passes `--suite draft` is not silently testing nothing. + */ +function noteIfDraftSuiteEmpty(suite: string, scenarios: string[]): void { + if (suite !== 'draft' || scenarios.length > 0) return; + console.error( + `Note: the draft suite is empty — no scenario targets a requirement newer than ${LATEST_SPEC_VERSION} yet. ` + + `Scenarios that used to live here shipped with ${LATEST_SPEC_VERSION} and now run in the default suite.` + ); +} + function filterScenariosBySpecVersion( allScenarios: string[], version: SpecVersion, @@ -228,17 +251,14 @@ program 'Path to YAML file listing expected failures (baseline)' ) .option('-o, --output-dir ', 'Save results to this directory') - .option( - '--spec-version ', - 'Filter scenarios by spec version (cumulative for date versions)' - ) + .option('--spec-version ', SPEC_VERSION_HELP) .option( '--force', 'Run a scenario even if it is not applicable at the requested --spec-version' ) .option( '--requirements ', - 'Run exactly the scenarios a spec revision requires, frozen at its release (e.g. 2026-07-28). Replaces --suite and --spec-version' + `Run exactly the scenarios a spec revision requires, frozen at its release (e.g. ${LATEST_SPEC_VERSION}). Replaces --suite and --spec-version` ) .option('--verbose', 'Show verbose output') .action(async (options, cmd) => { @@ -303,6 +323,7 @@ program } scenarios = suites[suiteName](); + noteIfDraftSuiteEmpty(suiteName, scenarios); if (specVersionFilter) { scenarios = filterScenariosBySpecVersion( scenarios, @@ -543,17 +564,14 @@ program 'Path to YAML file listing expected failures (baseline)' ) .option('-o, --output-dir ', 'Save results to this directory') - .option( - '--spec-version ', - 'Filter scenarios by spec version (cumulative for date versions)' - ) + .option('--spec-version ', SPEC_VERSION_HELP) .option( '--force', 'Run a scenario even if it is not applicable at the requested --spec-version' ) .option( '--requirements ', - 'Run exactly the scenarios a spec revision requires, frozen at its release (e.g. 2026-07-28). Replaces --suite and --spec-version' + `Run exactly the scenarios a spec revision requires, frozen at its release (e.g. ${LATEST_SPEC_VERSION}). Replaces --suite and --spec-version` ) .option('--timeout ', 'Per-scenario timeout in milliseconds', '30000') .option('--verbose', 'Show verbose output (JSON instead of pretty print)') @@ -641,9 +659,11 @@ program } else if (suite === 'pending') { scenarios = listPendingClientScenarios(); } else if (suite === 'draft') { - // Scenarios targeting the in-progress draft spec; excluded from - // 'active' until the draft is published as a dated release. + // Scenarios targeting requirements newer than the latest release; + // excluded from 'active' until that draft is published as a dated + // release and they are retagged to it. scenarios = listDraftClientScenarios(); + noteIfDraftSuiteEmpty(suite, scenarios); } else { console.error(`Unknown suite: ${suite}`); console.error('Available suites: active, all, core, draft, pending'); @@ -774,10 +794,7 @@ program 3000 ) .option('-o, --output-dir ', 'Save results to this directory') - .option( - '--spec-version ', - 'Filter scenarios by spec version (cumulative for date versions)' - ) + .option('--spec-version ', SPEC_VERSION_HELP) .option('--verbose', 'Show verbose output (JSON instead of pretty print)') .action(async (options) => { try { @@ -934,10 +951,7 @@ program .option('--client', 'List client scenarios') .option('--server', 'List server scenarios') .option('--authorization', 'List authorization server scenarios') - .option( - '--spec-version ', - 'Filter scenarios by spec version (cumulative for date versions)' - ) + .option('--spec-version ', SPEC_VERSION_HELP) .option( '--requirements ', 'List exactly what a spec revision requires, frozen at its release' diff --git a/src/mock-server/index.ts b/src/mock-server/index.ts index 7c8470e6..4660fa6e 100644 --- a/src/mock-server/index.ts +++ b/src/mock-server/index.ts @@ -59,7 +59,7 @@ export { createServerStateful } from './stateful'; export { createServerStateless, validateStatelessRequest, - withRequiredDraftResultFields, + withRequiredResultFields, CACHEABLE_RESULT_METHODS } from './stateless'; export { createServerFor } from './select'; diff --git a/src/mock-server/mock-server.test.ts b/src/mock-server/mock-server.test.ts index 43ecca64..4cf77a3c 100644 --- a/src/mock-server/mock-server.test.ts +++ b/src/mock-server/mock-server.test.ts @@ -4,19 +4,23 @@ import { createServerStateful } from './stateful'; import { createServerStateless, validateStatelessRequest, - withRequiredDraftResultFields, + withRequiredResultFields, CACHEABLE_RESULT_METHODS } from './stateless'; -import { STATELESS_SPEC_VERSIONS } from '../connection/select'; -import { LATEST_SPEC_VERSION, DRAFT_PROTOCOL_VERSION } from '../types'; +import { STATELESS_PROTOCOL_VERSIONS } from '../connection/select'; import { takeWireViolations } from '../validation/wire-schema'; +// The stateless mock is exercised at the first stateless release; the +// stateful mock at the last stateful one. +const STATELESS = '2026-07-28' as const; +const STATEFUL = '2025-11-25' as const; + const meta = { - 'io.modelcontextprotocol/protocolVersion': DRAFT_PROTOCOL_VERSION, + 'io.modelcontextprotocol/protocolVersion': STATELESS, 'io.modelcontextprotocol/clientInfo': { name: 't', version: '1' }, 'io.modelcontextprotocol/clientCapabilities': {} }; -const headers = { 'mcp-protocol-version': DRAFT_PROTOCOL_VERSION }; +const headers = { 'mcp-protocol-version': STATELESS }; async function post(url: string, body: object, headers: object = {}) { const r = await fetch(url, { @@ -40,7 +44,7 @@ describe('validateStatelessRequest', () => { } }, {}, - [DRAFT_PROTOCOL_VERSION] + [STATELESS] ); expect(v).toMatchObject({ kind: 'reject', status: 400 }); }); @@ -57,7 +61,7 @@ describe('validateStatelessRequest', () => { } }, {}, - [DRAFT_PROTOCOL_VERSION] + [STATELESS] ); expect(v).toMatchObject({ kind: 'handled', status: 200 }); }); @@ -74,7 +78,7 @@ describe('validateStatelessRequest', () => { } }, {}, - [DRAFT_PROTOCOL_VERSION] + [STATELESS] ); expect(v).toMatchObject({ kind: 'route', id: 1, method: 'tools/list' }); }); @@ -91,7 +95,7 @@ describe('validateStatelessRequest', () => { } }, {}, - [DRAFT_PROTOCOL_VERSION] + [STATELESS] ); expect(v).toMatchObject({ kind: 'handled', @@ -118,7 +122,7 @@ describe('validateStatelessRequest', () => { } }, {}, - [DRAFT_PROTOCOL_VERSION] + [STATELESS] ); expect(v).toMatchObject({ kind: 'reject', @@ -126,23 +130,24 @@ describe('validateStatelessRequest', () => { body: { error: { code: -32022, - data: { supported: [DRAFT_PROTOCOL_VERSION], requested: '2099-01-01' } + data: { supported: [STATELESS], requested: '2099-01-01' } } } }); }); }); -describe('withRequiredDraftResultFields', () => { +describe('withRequiredResultFields', () => { it('stamps resultType "complete" when the handler omitted it', () => { - expect( - withRequiredDraftResultFields('tools/call', { content: [] }) - ).toEqual({ resultType: 'complete', content: [] }); + expect(withRequiredResultFields('tools/call', { content: [] })).toEqual({ + resultType: 'complete', + content: [] + }); }); it('adds ttlMs and cacheScope for every cacheable method', () => { for (const method of CACHEABLE_RESULT_METHODS) { - expect(withRequiredDraftResultFields(method, {})).toEqual({ + expect(withRequiredResultFields(method, {})).toEqual({ resultType: 'complete', ttlMs: 0, cacheScope: 'private' @@ -151,7 +156,7 @@ describe('withRequiredDraftResultFields', () => { }); it('does not add caching hints to non-cacheable results', () => { - const result = withRequiredDraftResultFields('tools/call', { + const result = withRequiredResultFields('tools/call', { content: [] }); expect(result).not.toHaveProperty('ttlMs'); @@ -160,13 +165,13 @@ describe('withRequiredDraftResultFields', () => { it('preserves members the handler set itself', () => { expect( - withRequiredDraftResultFields('tools/call', { + withRequiredResultFields('tools/call', { resultType: 'input_required', inputRequests: {} }) ).toMatchObject({ resultType: 'input_required' }); expect( - withRequiredDraftResultFields('tools/list', { + withRequiredResultFields('tools/list', { ttlMs: 5000, cacheScope: 'public', tools: [] @@ -175,8 +180,8 @@ describe('withRequiredDraftResultFields', () => { }); it('passes non-object results through untouched', () => { - expect(withRequiredDraftResultFields('tools/call', null)).toBeNull(); - expect(withRequiredDraftResultFields('tools/call', [1])).toEqual([1]); + expect(withRequiredResultFields('tools/call', null)).toBeNull(); + expect(withRequiredResultFields('tools/call', [1])).toEqual([1]); }); }); @@ -210,7 +215,7 @@ describe('createServerFor', () => { } }); it('returns a stateless factory bound to the requested version', async () => { - const srv = await createServerFor(DRAFT_PROTOCOL_VERSION)({}); + const srv = await createServerFor(STATELESS)({}); try { const { status, body } = await post( srv.url, @@ -223,7 +228,7 @@ describe('createServerFor', () => { headers ); expect(status).toBe(200); - expect(body.result.supportedVersions).toEqual([DRAFT_PROTOCOL_VERSION]); + expect(body.result.supportedVersions).toEqual([STATELESS]); } finally { await srv.close(); } @@ -253,7 +258,7 @@ describe('createServerStateless', () => { const { status, body } = await post( srv.url, { jsonrpc: '2.0', id: 1, method: 'tools/list', params: {} }, - { 'mcp-protocol-version': DRAFT_PROTOCOL_VERSION } + { 'mcp-protocol-version': STATELESS } ); expect(status).toBe(400); expect(body.error.code).toBe(-32602); @@ -264,7 +269,7 @@ describe('createServerStateless', () => { } }); - it('serves server/discover, defaulting to every known stateless version', async () => { + it('serves server/discover, defaulting to every known stateless wire version', async () => { const srv = await createServerStateless({}); try { const { status, body } = await post( @@ -275,10 +280,12 @@ describe('createServerStateless', () => { method: 'server/discover', params: { _meta: meta } }, - { 'mcp-protocol-version': DRAFT_PROTOCOL_VERSION } + { 'mcp-protocol-version': STATELESS } ); expect(status).toBe(200); - expect(body.result.supportedVersions).toEqual(STATELESS_SPEC_VERSIONS); + expect(body.result.supportedVersions).toEqual( + STATELESS_PROTOCOL_VERSIONS + ); // Spec PR #3002: server identity lives in the result `_meta`. expect(body.result._meta['io.modelcontextprotocol/serverInfo'].name).toBe( 'conformance-mock-server' @@ -291,7 +298,7 @@ describe('createServerStateless', () => { it('accepts the version it was created for and rejects others with -32022', async () => { const srv = await createServerStateless( { 'tools/list': () => ({ tools: [] }) }, - DRAFT_PROTOCOL_VERSION + STATELESS ); try { const accepted = await post( @@ -323,9 +330,7 @@ describe('createServerStateless', () => { ); expect(rejected.status).toBe(400); expect(rejected.body.error.code).toBe(-32022); - expect(rejected.body.error.data.supported).toEqual([ - DRAFT_PROTOCOL_VERSION - ]); + expect(rejected.body.error.data.supported).toEqual([STATELESS]); expect(rejected.body.error.data.requested).toBe('2099-01-01'); } finally { await srv.close(); @@ -347,7 +352,7 @@ describe('createServerStateless', () => { method: 'tools/list', params: { _meta: meta } }, - { 'mcp-protocol-version': DRAFT_PROTOCOL_VERSION } + { 'mcp-protocol-version': STATELESS } ); expect(body.result.tools[0].name).toBe('x'); expect(srv.recorded).toHaveLength(1); @@ -363,7 +368,7 @@ describe('createServerStateless', () => { const { status } = await post( srv.url, { jsonrpc: '2.0', id: 1, method: 'tools/list', params: {} }, - { 'mcp-protocol-version': DRAFT_PROTOCOL_VERSION } + { 'mcp-protocol-version': STATELESS } ); expect(status).toBe(400); expect(srv.recorded.map((r) => r.method)).toEqual(['tools/list']); @@ -385,7 +390,7 @@ describe('createServerStateless', () => { method: 'server/discover', params: { _meta: meta } }, - { 'mcp-protocol-version': DRAFT_PROTOCOL_VERSION } + { 'mcp-protocol-version': STATELESS } ); expect(status).toBe(200); expect(srv.recorded).toHaveLength(0); @@ -464,7 +469,7 @@ describe('createServerStateless', () => { const { status, body } = await post( srv.url, { jsonrpc: '2.0', id: 1, method: 'nope', params: { _meta: meta } }, - { 'mcp-protocol-version': DRAFT_PROTOCOL_VERSION } + { 'mcp-protocol-version': STATELESS } ); expect(status).toBe(404); expect(body.error.code).toBe(-32601); @@ -502,7 +507,7 @@ describe('createServerStateful', () => { it('accepts initialize and routes to handlers, recording non-preamble', async () => { const srv = await createServerStateful( { 'tools/list': () => ({ tools: [] }) }, - LATEST_SPEC_VERSION + STATEFUL ); try { // SDK transport in sessionless mode handles initialize internally; we @@ -527,7 +532,7 @@ describe('createServerStateful', () => { it('derives capabilities from handler keys; non-tools handler does not 500 initialize', async () => { const srv = await createServerStateful( { 'prompts/list': () => ({ prompts: [] }) }, - LATEST_SPEC_VERSION + STATEFUL ); try { const { status, contentType } = await postInit(srv.url); @@ -541,7 +546,7 @@ describe('createServerStateful', () => { it('records requests for unregistered methods (parity with stateless)', async () => { const srv = await createServerStateful( { 'tools/list': () => ({ tools: [] }) }, - LATEST_SPEC_VERSION + STATEFUL ); try { const { Client } = @@ -571,7 +576,7 @@ describe('createServerStateful', () => { describe('wire violation attribution', () => { it('does not blame the harness for the JSON-RPC-mandated id:null on error replies', async () => { - const srv = await createServerStateless({}, DRAFT_PROTOCOL_VERSION); + const srv = await createServerStateless({}, STATELESS); try { // An id-less, _meta-less message forces a reject reply carrying // `id: null` (JSON-RPC 2.0 when the request id cannot be determined). @@ -584,8 +589,8 @@ describe('wire violation attribution', () => { }); it('does not record an implementation violation for an unparsed body (wrong Content-Type)', async () => { - const stateless = await createServerStateless({}, DRAFT_PROTOCOL_VERSION); - const stateful = await createServerStateful({}, LATEST_SPEC_VERSION); + const stateless = await createServerStateless({}, STATELESS); + const stateful = await createServerStateful({}, STATEFUL); try { for (const url of [stateless.url, stateful.url]) { await fetch(url, { diff --git a/src/mock-server/stateless.ts b/src/mock-server/stateless.ts index 3d06c86a..856843c7 100644 --- a/src/mock-server/stateless.ts +++ b/src/mock-server/stateless.ts @@ -9,10 +9,14 @@ */ import express from 'express'; -import { DRAFT_PROTOCOL_VERSION, type SpecVersion } from '../types'; +import { + LATEST_SPEC_VERSION, + protocolVersionFor, + type SpecVersion +} from '../types'; import type { JSONRPCRequest } from '../spec-types/2025-11-25'; import type { MockServer, RequestHandlers } from './index'; -import { STATELESS_SPEC_VERSIONS } from '../connection/select'; +import { STATELESS_PROTOCOL_VERSIONS } from '../connection/select'; import { validateWireMessage } from '../validation/wire-schema'; import { capabilitiesFromHandlers } from './stateful'; @@ -29,7 +33,7 @@ const META_KEYS = [ /** * Operations whose results the 2026-07-28 revision marks cacheable: servers * MUST include the caching hints `ttlMs` and `cacheScope` on these results - * (draft `CacheableResult`, server/utilities/caching). + * (`CacheableResult`, server/utilities/caching). */ export const CACHEABLE_RESULT_METHODS: ReadonlySet = new Set([ 'server/discover', @@ -49,7 +53,7 @@ export const CACHEABLE_RESULT_METHODS: ReadonlySet = new Set([ * filled. A scenario that needs to send a deliberately non-conformant result * must build its own server instead of routing through this mock. */ -export function withRequiredDraftResultFields( +export function withRequiredResultFields( method: string, result: unknown ): unknown { @@ -159,7 +163,7 @@ export function validateStatelessRequest( body: { jsonrpc: '2.0', id, - result: withRequiredDraftResultFields(method, { + result: withRequiredResultFields(method, { supportedVersions, capabilities, // Spec PR #3002: server identity lives in the result `_meta`, not @@ -189,9 +193,10 @@ export async function createServerStateless( const recorded: JSONRPCRequest[] = []; const capabilities = capabilitiesFromHandlers(handlers); const supportedVersions: readonly string[] = specVersion - ? [specVersion] - : STATELESS_SPEC_VERSIONS; - const wireVersion = specVersion ?? DRAFT_PROTOCOL_VERSION; + ? [protocolVersionFor(specVersion)] + : STATELESS_PROTOCOL_VERSIONS; + // Schema to validate traffic against; the latest release when unpinned. + const schemaVersion: SpecVersion = specVersion ?? LATEST_SPEC_VERSION; const app = express(); app.use(express.json()); @@ -205,7 +210,7 @@ export async function createServerStateless( // An unparsed body (wrong or missing Content-Type) is a transport fault // surfaced by the HTTP-level checks, not a JSON-RPC message to validate. if (body !== undefined) { - validateWireMessage(wireVersion, req.body, { + validateWireMessage(schemaVersion, req.body, { origin: 'implementation', context: `client request '${body?.method ?? '(unknown)'}' to stateless mock` }); @@ -224,7 +229,7 @@ export async function createServerStateless( raw.error !== undefined && raw.id === null ? { ...raw, id: 0 } : payload; - validateWireMessage(wireVersion, validated, { + validateWireMessage(schemaVersion, validated, { origin: 'harness', context: `stateless mock response${method ? ` to '${method}'` : ''}`, requestMethod: method @@ -254,7 +259,7 @@ export async function createServerStateless( { jsonrpc: '2.0', id, - result: withRequiredDraftResultFields(method, result) + result: withRequiredResultFields(method, result) }, method ); diff --git a/src/mock-server/testing.ts b/src/mock-server/testing.ts index 1e307b8d..8dce3588 100644 --- a/src/mock-server/testing.ts +++ b/src/mock-server/testing.ts @@ -1,14 +1,21 @@ -import { LATEST_SPEC_VERSION, type SpecVersion } from '../types'; +import type { SpecVersion } from '../types'; import { createServerFor } from './select'; import type { ScenarioContext } from './index'; +/** + * Spec version unit tests run at when they do not name one: the last + * revision with the stateful lifecycle, which is what the bundled SDK + * `Client`/`Server` most tests drive scenarios with speak. Pass + * `'2026-07-28'` (or the draft) explicitly to exercise the stateless path. + */ +export const DEFAULT_TEST_SPEC_VERSION: SpecVersion = '2025-11-25'; + /** * Build a ScenarioContext for unit tests that drive a Scenario directly. - * Defaults to the latest dated spec version (stateful lifecycle) so existing - * tests keep their pre-ScenarioContext behaviour. + * Defaults to {@link DEFAULT_TEST_SPEC_VERSION}. */ export function testScenarioContext( - specVersion: SpecVersion = LATEST_SPEC_VERSION + specVersion: SpecVersion = DEFAULT_TEST_SPEC_VERSION ): ScenarioContext { return { specVersion, diff --git a/src/runner/client.test.ts b/src/runner/client.test.ts index 8ce464ac..7061cd39 100644 --- a/src/runner/client.test.ts +++ b/src/runner/client.test.ts @@ -6,7 +6,13 @@ */ import { describe, test, expect } from 'vitest'; import { runConformanceTest } from './client'; -import { DRAFT_PROTOCOL_VERSION, LATEST_SPEC_VERSION } from '../types'; +import { + DRAFT_PROTOCOL_VERSION, + DRAFT_SPEC_VERSION, + LATEST_SPEC_VERSION +} from '../types'; + +const LAST_STATEFUL = '2025-11-25' as const; // A "client" that just prints the protocol version handed to it, so tests // can observe which version the runner resolved. @@ -14,8 +20,8 @@ const PRINT_VERSION_COMMAND = 'node -e "console.log(process.env.MCP_CONFORMANCE_PROTOCOL_VERSION)"'; describe('runConformanceTest spec-version applicability', () => { - test('skips a draft-only scenario at an explicit dated spec version', async () => { - // http-custom-headers is introducedIn DRAFT, so a dated version + test('skips a 2026-07-28 scenario at an explicit earlier spec version', async () => { + // http-custom-headers is introducedIn 2026-07-28, so an earlier version // contradicts it. The skip happens before the mock server starts and // before the client command is spawned. const result = await runConformanceTest( @@ -23,7 +29,7 @@ describe('runConformanceTest spec-version applicability', () => { 'http-custom-headers', 5000, undefined, - LATEST_SPEC_VERSION + LAST_STATEFUL ); expect(result.skipped).toBe(true); expect(result.checks).toEqual([]); @@ -36,26 +42,28 @@ describe('runConformanceTest spec-version applicability', () => { 'http-custom-headers', 10000, undefined, - LATEST_SPEC_VERSION, + LAST_STATEFUL, true ); expect(result.skipped).toBeUndefined(); - expect(result.clientOutput?.stdout).toContain(LATEST_SPEC_VERSION); + expect(result.clientOutput?.stdout).toContain(LAST_STATEFUL); }, 30000); - test('infers the draft version for a draft-only scenario when --spec-version is omitted', async () => { + test('hands the client the draft wire version, not the word "draft", under --spec-version draft', async () => { const result = await runConformanceTest( PRINT_VERSION_COMMAND, 'http-custom-headers', - 10000 + 10000, + undefined, + DRAFT_SPEC_VERSION ); expect(result.skipped).toBeUndefined(); - expect(result.clientOutput?.stdout).toContain(DRAFT_PROTOCOL_VERSION); + expect(result.clientOutput?.stdout.trim()).toBe(DRAFT_PROTOCOL_VERSION); }, 30000); - test('infers the latest dated version for a dual-version scenario when --spec-version is omitted', async () => { - // tools_call is introducedIn 2025-06-18 and still applicable at draft; - // omitting --spec-version must keep the latest dated default. + test('infers the latest release for a scenario that applies there when --spec-version is omitted', async () => { + // tools_call is introducedIn 2025-06-18 and still applicable at the + // latest release; omitting --spec-version runs it there. const result = await runConformanceTest( PRINT_VERSION_COMMAND, 'tools_call', @@ -64,4 +72,17 @@ describe('runConformanceTest spec-version applicability', () => { expect(result.skipped).toBeUndefined(); expect(result.clientOutput?.stdout).toContain(LATEST_SPEC_VERSION); }, 30000); + + test('infers the last release before removedIn for a removed scenario when --spec-version is omitted', async () => { + // initialize is removedIn 2026-07-28; without --spec-version it runs at + // the newest revision it still applies to rather than being handed a + // stateless mock server it cannot handshake with. + const result = await runConformanceTest( + PRINT_VERSION_COMMAND, + 'initialize', + 10000 + ); + expect(result.skipped).toBeUndefined(); + expect(result.clientOutput?.stdout).toContain(LAST_STATEFUL); + }, 30000); }); diff --git a/src/runner/client.ts b/src/runner/client.ts index af10657f..6188b11e 100644 --- a/src/runner/client.ts +++ b/src/runner/client.ts @@ -6,9 +6,13 @@ import { ScenarioSource, SpecVersion, LATEST_SPEC_VERSION, - DRAFT_PROTOCOL_VERSION + protocolVersionFor } from '../types'; -import { getScenario, isScenarioApplicableAt } from '../scenarios'; +import { + defaultSpecVersionFor, + getScenario, + isScenarioApplicableAt +} from '../scenarios'; import { createServerFor, type ScenarioContext } from '../mock-server'; import { resetWireValidation, @@ -46,7 +50,9 @@ async function executeClient( // 3. Semantic separation: scenario identifies "which test", context provides "test data" const env = { ...process.env }; env.MCP_CONFORMANCE_SCENARIO = scenarioName; - env.MCP_CONFORMANCE_PROTOCOL_VERSION = specVersion ?? LATEST_SPEC_VERSION; + env.MCP_CONFORMANCE_PROTOCOL_VERSION = protocolVersionFor( + specVersion ?? LATEST_SPEC_VERSION + ); if (context) { // Include scenario name in context for discriminated union parsing env.MCP_CONFORMANCE_CONTEXT = JSON.stringify({ @@ -132,18 +138,14 @@ function shouldSkipForSpecVersion( } // When --spec-version is omitted, infer the version from the scenario's -// declared source so draft-only scenarios get the draft (stateless) mock -// server rather than the stateful latest-spec default. +// declared source so each scenario gets a mock server speaking a wire it was +// written for: the latest release when it applies there, otherwise the newest +// revision in its window (draft-only → draft, removed → last release before). function resolveScenarioSpecVersion( source: ScenarioSource, specVersion: SpecVersion | undefined ): SpecVersion { - return ( - specVersion ?? - ('introducedIn' in source && source.introducedIn === DRAFT_PROTOCOL_VERSION - ? DRAFT_PROTOCOL_VERSION - : LATEST_SPEC_VERSION) - ); + return specVersion ?? defaultSpecVersionFor(source); } export async function runConformanceTest( diff --git a/src/runner/server.test.ts b/src/runner/server.test.ts index 2f6ac645..5b3deb07 100644 --- a/src/runner/server.test.ts +++ b/src/runner/server.test.ts @@ -7,46 +7,50 @@ import http from 'http'; import type { AddressInfo } from 'net'; import { afterEach, beforeEach, describe, test, expect } from 'vitest'; import { runServerConformanceTest } from './server'; -import { DRAFT_PROTOCOL_VERSION, LATEST_SPEC_VERSION } from '../types'; +import { DRAFT_SPEC_VERSION, LATEST_SPEC_VERSION } from '../types'; // The skip decision happens before any network request, so an unreachable // URL proves the scenario was not run. const UNREACHABLE_URL = 'http://127.0.0.1:9/mcp'; describe('runServerConformanceTest spec-version applicability', () => { - test('skips a draft-only scenario at an explicit dated spec version', async () => { + test('skips a 2026-07-28 scenario at an explicit earlier spec version', async () => { const result = await runServerConformanceTest( UNREACHABLE_URL, 'server-stateless', undefined, - LATEST_SPEC_VERSION + '2025-11-25' ); expect(result.skipped).toBe(true); expect(result.checks).toEqual([]); }); - test('skips a removed-in-draft scenario at the draft spec version', async () => { - // server-initialize tests the stateful handshake, which the draft - // (stateless) lifecycle removed. - const result = await runServerConformanceTest( - UNREACHABLE_URL, - 'server-initialize', - undefined, - DRAFT_PROTOCOL_VERSION - ); - expect(result.skipped).toBe(true); - expect(result.checks).toEqual([]); - }); + test.each([LATEST_SPEC_VERSION, DRAFT_SPEC_VERSION] as const)( + 'skips a removed-in-2026-07-28 scenario at %s', + async (version) => { + // server-initialize tests the stateful handshake, which the 2026-07-28 + // (stateless) lifecycle removed; the draft comes after it. + const result = await runServerConformanceTest( + UNREACHABLE_URL, + 'server-initialize', + undefined, + version + ); + expect(result.skipped).toBe(true); + expect(result.checks).toEqual([]); + } + ); test('does not skip an applicable scenario/spec-version combination', async () => { - // server-stateless at draft is applicable; the runner proceeds to run it - // (against an unreachable server, so checks exist and report failures — - // the point is only that it was not skipped). + // server-stateless at the draft is applicable (introduced 2026-07-28, not + // removed); the runner proceeds to run it (against an unreachable server, + // so checks exist and report failures — the point is only that it was + // not skipped). const result = await runServerConformanceTest( UNREACHABLE_URL, 'server-stateless', undefined, - DRAFT_PROTOCOL_VERSION + DRAFT_SPEC_VERSION ); expect(result.skipped).toBeUndefined(); expect(result.checks.length).toBeGreaterThan(0); @@ -99,12 +103,12 @@ describe('runServerConformanceTest per-scenario timeout', () => { }, 30000); }); -describe('runServerConformanceTest wire selection for draft-only scenarios', () => { +describe('runServerConformanceTest wire selection for stateless-era scenarios', () => { // Regression: the CLI used to silently emit the legacy initialize+session - // wire when running a draft-only scenario, producing requests with no + // wire when running a 2026-07-28-only scenario, producing requests with no // `_meta.io.modelcontextprotocol/*` envelope (and `initialize` rather // than `server/discover`). Deriving wire from spec version on the - // RunContext makes the CLI emit SEP-2575 stateless traffic on draft. + // RunContext makes the CLI emit SEP-2575 stateless traffic there. let server: http.Server; let url: string; const captured: Array<{ method?: string; params?: Record }> = @@ -172,8 +176,9 @@ describe('runServerConformanceTest wire selection for draft-only scenarios', () const first = captured[0]; const meta = first.params?._meta as Record | undefined; expect(meta).toBeDefined(); + // Extension scenarios run at the latest release when no version is given. expect(meta?.['io.modelcontextprotocol/protocolVersion']).toBe( - DRAFT_PROTOCOL_VERSION + LATEST_SPEC_VERSION ); expect(meta?.['io.modelcontextprotocol/clientInfo']).toBeDefined(); expect(meta?.['io.modelcontextprotocol/clientCapabilities']).toBeDefined(); diff --git a/src/runner/server.ts b/src/runner/server.ts index 21e80ec4..dc684fb1 100644 --- a/src/runner/server.ts +++ b/src/runner/server.ts @@ -1,12 +1,11 @@ import { promises as fs } from 'fs'; import path from 'path'; +import { ConformanceCheck, SpecVersion } from '../types'; import { - ConformanceCheck, - SpecVersion, - LATEST_SPEC_VERSION, - DRAFT_PROTOCOL_VERSION -} from '../types'; -import { getClientScenario, isScenarioApplicableAt } from '../scenarios'; + defaultSpecVersionFor, + getClientScenario, + isScenarioApplicableAt +} from '../scenarios'; import { connectFor, type RunContext } from '../connection'; import { resetWireValidation, @@ -124,16 +123,13 @@ export async function runServerConformanceTest( } // When --spec-version is omitted, infer the version from the scenario's - // declared source so draft-only scenarios get the draft (stateless) - // connection rather than the stateful latest-spec default. Extension - // scenarios are off-timeline; today every extension in this repo lives - // on draft, so they fall under the same inference. + // declared source so each scenario gets a connection speaking a wire it was + // written for: the latest release when it applies there, otherwise the + // newest revision in its window (draft-only → draft, removed → the last + // release before). Extension scenarios are off-timeline and run at the + // latest release. const resolvedSpecVersion = - specVersion ?? - ('extensionId' in scenario.source || - scenario.source.introducedIn === DRAFT_PROTOCOL_VERSION - ? DRAFT_PROTOCOL_VERSION - : LATEST_SPEC_VERSION); + specVersion ?? defaultSpecVersionFor(scenario.source); console.log( `Running client scenario '${scenarioName}' against server: ${serverUrl}` diff --git a/src/scenarios/client/auth/authorization-server-migration.ts b/src/scenarios/client/auth/authorization-server-migration.ts index 6c7c989e..20570684 100644 --- a/src/scenarios/client/auth/authorization-server-migration.ts +++ b/src/scenarios/client/auth/authorization-server-migration.ts @@ -9,7 +9,7 @@ import type { ScenarioContext } from '../../../mock-server'; import type { Request, Response, NextFunction } from 'express'; import type { Scenario, ConformanceCheck } from '../../../types'; -import { ScenarioUrls, DRAFT_PROTOCOL_VERSION } from '../../../types'; +import { ScenarioUrls } from '../../../types'; import { createAuthServer } from './helpers/createAuthServer'; import { createServer } from './helpers/createServer'; import { ServerLifecycle } from './helpers/serverLifecycle'; @@ -18,7 +18,7 @@ import { SpecReferences } from './spec-references'; export class AuthorizationServerMigrationScenario implements Scenario { name = 'auth/authorization-server-migration'; - readonly source = { introducedIn: DRAFT_PROTOCOL_VERSION } as const; + readonly source = { introducedIn: '2026-07-28' } as const; description = 'Tests that a client, when the PRM authorization_servers changes to a new issuer, re-registers with the new authorization server and does not reuse credentials from the previous one (SEP-2352).'; private as1 = new ServerLifecycle(); diff --git a/src/scenarios/client/auth/helpers/createAuthServer.ts b/src/scenarios/client/auth/helpers/createAuthServer.ts index 162f93aa..ac35c57e 100644 --- a/src/scenarios/client/auth/helpers/createAuthServer.ts +++ b/src/scenarios/client/auth/helpers/createAuthServer.ts @@ -2,7 +2,7 @@ import express, { Request, Response } from 'express'; import { createHash } from 'crypto'; import type { ConformanceCheck } from '../../../../types'; import type { ScenarioContext } from '../../../../mock-server'; -import { isStatefulVersion } from '../../../../connection/select'; +import { specVersionAtLeast } from '../../../../types'; import { createRequestLogger } from '../../../request-logger'; import { SpecReferences } from '../spec-references'; import { MockTokenVerifier } from './mockTokenVerifier'; @@ -752,11 +752,10 @@ export function createAuthServer( // SEP-837: clients MUST specify an appropriate application_type during DCR. // The harness can't know the client's real class (native vs web), so this // checks presence + that the value is one of the two OIDC-defined values. - // SEP-837 first appears in the draft spec (the same revision that - // introduces the stateless lifecycle), so the check only exists for runs - // targeting a version that includes it; at dated versions it is not + // SEP-837 first appears in 2026-07-28, so the check only exists for runs + // targeting a version that includes it; at earlier versions it is not // emitted at all. - if (!isStatefulVersion(ctx.specVersion)) { + if (specVersionAtLeast(ctx.specVersion, '2026-07-28')) { const appType = req.body.application_type; const validAppType = appType === 'native' || appType === 'web'; checks.push({ diff --git a/src/scenarios/client/auth/helpers/createServer.ts b/src/scenarios/client/auth/helpers/createServer.ts index 6d308d64..890e9fb9 100644 --- a/src/scenarios/client/auth/helpers/createServer.ts +++ b/src/scenarios/client/auth/helpers/createServer.ts @@ -10,9 +10,10 @@ import { import { requireBearerAuth } from '@modelcontextprotocol/sdk/server/auth/middleware/bearerAuth.js'; import express, { Request, Response, NextFunction } from 'express'; import type { ConformanceCheck } from '../../../../types'; +import { protocolVersionFor } from '../../../../types'; import { validateStatelessRequest, - withRequiredDraftResultFields, + withRequiredResultFields, type ScenarioContext } from '../../../../mock-server'; import { isStatefulVersion } from '../../../../connection/select'; @@ -207,7 +208,9 @@ export function createServer( // as createMcpServer. Bearer-auth middleware and PRM route above are // version-independent. function handleStateless(req: Request, res: Response) { - const v = validateStatelessRequest(req, { tools: {} }, [ctx.specVersion]); + const v = validateStatelessRequest(req, { tools: {} }, [ + protocolVersionFor(ctx.specVersion) + ]); if (v.kind !== 'route') { return res.status(v.status).json(v.body); } @@ -216,7 +219,7 @@ export function createServer( return res.json({ jsonrpc: '2.0', id, - result: withRequiredDraftResultFields(method, { + result: withRequiredResultFields(method, { tools: [{ name: 'test-tool', inputSchema: { type: 'object' } }] }) }); @@ -225,7 +228,7 @@ export function createServer( return res.json({ jsonrpc: '2.0', id, - result: withRequiredDraftResultFields(method, { + result: withRequiredResultFields(method, { content: [{ type: 'text', text: 'test' }] }) }); diff --git a/src/scenarios/client/auth/index.test.ts b/src/scenarios/client/auth/index.test.ts index 41c84ae8..38982999 100644 --- a/src/scenarios/client/auth/index.test.ts +++ b/src/scenarios/client/auth/index.test.ts @@ -1,7 +1,6 @@ import { authScenariosList, backcompatScenariosList, - draftScenariosList, extensionScenariosList } from './index'; import { @@ -39,7 +38,6 @@ import { runClient as dpopClient } from '../../../../examples/clients/typescript import { runClient as resourceSlashClient } from '../../../../examples/clients/typescript/auth-test-resource-slash'; import { getHandler } from '../../../../examples/clients/typescript/everything-client'; import { setLogLevel } from '../../../../examples/clients/typescript/helpers/logger'; -import { DRAFT_PROTOCOL_VERSION } from '../../../types'; import { testScenarioContext } from '../../../mock-server/testing'; import { ClientConformanceContextSchema } from '../../../schemas/context'; @@ -117,21 +115,6 @@ describe('Client Back-compat Scenarios', () => { } }); -describe('Client Draft Scenarios', () => { - for (const scenario of draftScenariosList) { - test(`${scenario.name} passes`, async () => { - const clientFn = getHandler(scenario.name); - if (!clientFn) { - throw new Error(`No handler registered for scenario: ${scenario.name}`); - } - const runner = new InlineClientRunner(clientFn); - await runClientAgainstScenario(runner, scenario.name, { - allowClientError: allowClientErrorScenarios.has(scenario.name) - }); - }); - } -}); - describe('auth/pre-registration context', () => { // Authorization Server Binding: the context issuer is only usable as a // binding key if it equals the issuer the mock AS publishes in its metadata. @@ -206,12 +189,12 @@ describe('Negative tests', () => { }); test('client only responds to 401, not 403', async () => { - // Run at draft so the SEP-2350 union check is also emitted: a client that - // never makes the second authorization request fails both the escalation - // check and the union check. + // Run at 2026-07-28 so the SEP-2350 union check is also emitted: a client + // that never makes the second authorization request fails both the + // escalation check and the union check. const runner = new InlineClientRunner(ignore403Client); await runClientAgainstScenario(runner, 'auth/scope-step-up', { - specVersion: DRAFT_PROTOCOL_VERSION, + specVersion: '2026-07-28', expectedFailureSlugs: [ 'scope-step-up-escalation', 'sep-2350-scope-union-on-reauth' @@ -219,12 +202,12 @@ describe('Negative tests', () => { }); }); - test('client echoes challenge scope without accumulating prior grant (SEP-2350) at draft', async () => { - // The set-wise union requirement (SEP-2350) was added in the draft spec - // (2026-07-28); a client that echoes only the challenged scope fails it. + test('client echoes challenge scope without accumulating prior grant (SEP-2350) at 2026-07-28', async () => { + // The set-wise union requirement (SEP-2350) was added in 2026-07-28; a + // client that echoes only the challenged scope fails it. const runner = new InlineClientRunner(echoScopeClient); await runClientAgainstScenario(runner, 'auth/scope-step-up', { - specVersion: DRAFT_PROTOCOL_VERSION, + specVersion: '2026-07-28', expectedFailureSlugs: ['sep-2350-scope-union-on-reauth'] }); }); @@ -270,12 +253,12 @@ describe('Negative tests', () => { ); }); - test('client omits application_type during DCR (SEP-837) at draft', async () => { - // SEP-837 is a draft-spec requirement, so the check is only enforced when - // the run targets the draft version. + test('client omits application_type during DCR (SEP-837) at 2026-07-28', async () => { + // SEP-837 is a 2026-07-28 requirement, so the check is only enforced when + // the run targets that revision or later. const runner = new InlineClientRunner(noAppTypeClient); await runClientAgainstScenario(runner, 'auth/metadata-default', { - specVersion: DRAFT_PROTOCOL_VERSION, + specVersion: '2026-07-28', expectedFailureSlugs: ['sep-837-application-type-present'] }); }); diff --git a/src/scenarios/client/auth/index.ts b/src/scenarios/client/auth/index.ts index 7daec39b..e5830809 100644 --- a/src/scenarios/client/auth/index.ts +++ b/src/scenarios/client/auth/index.ts @@ -41,7 +41,8 @@ import { MetadataIssuerMismatchScenario } from './issuer-parameter'; -// Auth scenarios (required for tier 1) +// Auth scenarios on the dated spec timeline (required for tier 1 at the +// revisions they apply to; `--spec-version` narrows the set per revision). export const authScenariosList: Scenario[] = [ ...metadataScenarios, new AuthBasicCIMDScenario(), @@ -53,7 +54,19 @@ export const authScenariosList: Scenario[] = [ new ClientSecretBasicAuthScenario(), new ClientSecretPostAuthScenario(), new PublicClientAuthScenario(), - new PreRegistrationScenario() + new PreRegistrationScenario(), + // Introduced in 2026-07-28 + new ResourceMismatchScenario(), + new OfflineAccessScopeScenario(), + new OfflineAccessNotSupportedScenario(), + new AuthorizationServerMigrationScenario(), + new IssParameterSupportedScenario(), + new IssParameterNotAdvertisedScenario(), + new IssParameterSupportedMissingScenario(), + new IssParameterWrongIssuerScenario(), + new IssParameterUnexpectedScenario(), + new IssParameterNormalizedVariantScenario(), + new MetadataIssuerMismatchScenario() ]; // Back-compat scenarios (optional - backward compatibility with older spec versions) @@ -71,18 +84,3 @@ export const extensionScenariosList: Scenario[] = [ new DPoPClientScenario(true), // auth/dpop-nonce — server-required nonce (§8/§9) new WifJwtBearerScenario() ]; - -// Draft scenarios (informational - not scored for tier assessment) -export const draftScenariosList: Scenario[] = [ - new ResourceMismatchScenario(), - new OfflineAccessScopeScenario(), - new OfflineAccessNotSupportedScenario(), - new AuthorizationServerMigrationScenario(), - new IssParameterSupportedScenario(), - new IssParameterNotAdvertisedScenario(), - new IssParameterSupportedMissingScenario(), - new IssParameterWrongIssuerScenario(), - new IssParameterUnexpectedScenario(), - new IssParameterNormalizedVariantScenario(), - new MetadataIssuerMismatchScenario() -]; diff --git a/src/scenarios/client/auth/issuer-parameter.ts b/src/scenarios/client/auth/issuer-parameter.ts index 85773e23..ea1c4909 100644 --- a/src/scenarios/client/auth/issuer-parameter.ts +++ b/src/scenarios/client/auth/issuer-parameter.ts @@ -1,6 +1,6 @@ import type { ScenarioContext } from '../../../mock-server'; import type { Scenario, ConformanceCheck } from '../../../types.js'; -import { ScenarioUrls, DRAFT_PROTOCOL_VERSION } from '../../../types.js'; +import { ScenarioUrls } from '../../../types.js'; import { createAuthServer } from './helpers/createAuthServer.js'; import { createServer } from './helpers/createServer.js'; import { ServerLifecycle } from './helpers/serverLifecycle.js'; @@ -110,7 +110,7 @@ function issRejectionCheck(opts: { */ export class IssParameterSupportedScenario implements Scenario { name = 'auth/iss-supported'; - readonly source = { introducedIn: DRAFT_PROTOCOL_VERSION } as const; + readonly source = { introducedIn: '2026-07-28' } as const; description = 'Tests that client accepts authorization response when server advertises and sends correct iss parameter'; @@ -180,7 +180,7 @@ export class IssParameterSupportedScenario implements Scenario { */ export class IssParameterNotAdvertisedScenario implements Scenario { name = 'auth/iss-not-advertised'; - readonly source = { introducedIn: DRAFT_PROTOCOL_VERSION } as const; + readonly source = { introducedIn: '2026-07-28' } as const; description = 'Tests that client accepts authorization response when server does not advertise or send iss parameter'; @@ -250,7 +250,7 @@ export class IssParameterNotAdvertisedScenario implements Scenario { */ export class IssParameterSupportedMissingScenario implements Scenario { name = 'auth/iss-supported-missing'; - readonly source = { introducedIn: DRAFT_PROTOCOL_VERSION } as const; + readonly source = { introducedIn: '2026-07-28' } as const; description = 'Tests that client rejects authorization response when server advertised iss support but omitted iss from redirect'; allowClientError = true; @@ -340,7 +340,7 @@ export class IssParameterSupportedMissingScenario implements Scenario { */ export class IssParameterWrongIssuerScenario implements Scenario { name = 'auth/iss-wrong-issuer'; - readonly source = { introducedIn: DRAFT_PROTOCOL_VERSION } as const; + readonly source = { introducedIn: '2026-07-28' } as const; description = 'Tests that client rejects authorization response when iss does not match the authorization server issuer'; allowClientError = true; @@ -431,7 +431,7 @@ export class IssParameterWrongIssuerScenario implements Scenario { */ export class IssParameterUnexpectedScenario implements Scenario { name = 'auth/iss-unexpected'; - readonly source = { introducedIn: DRAFT_PROTOCOL_VERSION } as const; + readonly source = { introducedIn: '2026-07-28' } as const; description = 'Tests that client compares iss against recorded issuer even when not advertised, and rejects on mismatch'; allowClientError = true; @@ -527,7 +527,7 @@ export class IssParameterUnexpectedScenario implements Scenario { */ export class IssParameterNormalizedVariantScenario implements Scenario { name = 'auth/iss-normalized'; - readonly source = { introducedIn: DRAFT_PROTOCOL_VERSION } as const; + readonly source = { introducedIn: '2026-07-28' } as const; description = 'Tests that client compares iss using simple string comparison without applying URL normalization'; allowClientError = true; @@ -619,7 +619,7 @@ export class IssParameterNormalizedVariantScenario implements Scenario { */ export class MetadataIssuerMismatchScenario implements Scenario { name = 'auth/metadata-issuer-mismatch'; - readonly source = { introducedIn: DRAFT_PROTOCOL_VERSION } as const; + readonly source = { introducedIn: '2026-07-28' } as const; description = 'Tests that client rejects authorization server metadata whose issuer does not match the issuer used to construct the well-known URL'; allowClientError = true; diff --git a/src/scenarios/client/auth/offline-access.ts b/src/scenarios/client/auth/offline-access.ts index 77eccca5..c8cdb838 100644 --- a/src/scenarios/client/auth/offline-access.ts +++ b/src/scenarios/client/auth/offline-access.ts @@ -1,6 +1,6 @@ import type { ScenarioContext } from '../../../mock-server'; import type { Scenario, ConformanceCheck } from '../../../types'; -import { ScenarioUrls, DRAFT_PROTOCOL_VERSION } from '../../../types'; +import { ScenarioUrls } from '../../../types'; import { createAuthServer } from './helpers/createAuthServer'; import { createServer } from './helpers/createServer'; import { ServerLifecycle } from './helpers/serverLifecycle'; @@ -24,7 +24,7 @@ import { MockTokenVerifier } from './helpers/mockTokenVerifier'; */ export class OfflineAccessScopeScenario implements Scenario { name = 'auth/offline-access-scope'; - readonly source = { introducedIn: DRAFT_PROTOCOL_VERSION } as const; + readonly source = { introducedIn: '2026-07-28' } as const; description = 'Tests that a client that wants a refresh token handles offline_access scope and refresh_token grant type when AS supports them (SEP-2207)'; @@ -229,7 +229,7 @@ export class OfflineAccessScopeScenario implements Scenario { */ export class OfflineAccessNotSupportedScenario implements Scenario { name = 'auth/offline-access-not-supported'; - readonly source = { introducedIn: DRAFT_PROTOCOL_VERSION } as const; + readonly source = { introducedIn: '2026-07-28' } as const; description = 'Tests that client does not request offline_access when AS does not list it in scopes_supported (SEP-2207)'; diff --git a/src/scenarios/client/auth/resource-mismatch.ts b/src/scenarios/client/auth/resource-mismatch.ts index a5cc157a..19315fe6 100644 --- a/src/scenarios/client/auth/resource-mismatch.ts +++ b/src/scenarios/client/auth/resource-mismatch.ts @@ -1,6 +1,6 @@ import type { ScenarioContext } from '../../../mock-server'; import type { Scenario, ConformanceCheck } from '../../../types.js'; -import { ScenarioUrls, DRAFT_PROTOCOL_VERSION } from '../../../types.js'; +import { ScenarioUrls } from '../../../types.js'; import { createAuthServer } from './helpers/createAuthServer.js'; import { createServer } from './helpers/createServer.js'; import { ServerLifecycle } from './helpers/serverLifecycle.js'; @@ -29,7 +29,7 @@ import { untestableCheck } from '../../untestable.js'; */ export class ResourceMismatchScenario implements Scenario { name = 'auth/resource-mismatch'; - readonly source = { introducedIn: DRAFT_PROTOCOL_VERSION } as const; + readonly source = { introducedIn: '2026-07-28' } as const; description = 'Tests that client rejects when PRM resource does not match server URL'; allowClientError = true; diff --git a/src/scenarios/client/auth/scope-handling.ts b/src/scenarios/client/auth/scope-handling.ts index 227c6c45..45d5e123 100644 --- a/src/scenarios/client/auth/scope-handling.ts +++ b/src/scenarios/client/auth/scope-handling.ts @@ -6,7 +6,7 @@ import { createServer } from './helpers/createServer'; import { ServerLifecycle } from './helpers/serverLifecycle'; import { SpecReferences } from './spec-references'; import { MockTokenVerifier } from './helpers/mockTokenVerifier'; -import { DRAFT_PROTOCOL_VERSION, specVersionAtLeast } from '../../../types'; +import { specVersionAtLeast } from '../../../types'; import type { Request, Response, NextFunction } from 'express'; /** @@ -295,17 +295,14 @@ export class ScopeStepUpAuthScenario implements Scenario { private authServer = new ServerLifecycle(); private server = new ServerLifecycle(); private checks: ConformanceCheck[] = []; - // SEP-2350's set-wise union requirement was introduced in 2026-07-28 (the - // current draft); it was not a requirement at 2025-11-25, where - // non-accumulating re-auth is conformant. Gate the union check accordingly. + // SEP-2350's set-wise union requirement was introduced in 2026-07-28; it + // was not a requirement at 2025-11-25, where non-accumulating re-auth is + // conformant. Gate the union check accordingly. private unionRequired = false; async start(ctx: ScenarioContext): Promise { this.checks = []; - this.unionRequired = specVersionAtLeast( - ctx.specVersion, - DRAFT_PROTOCOL_VERSION - ); + this.unionRequired = specVersionAtLeast(ctx.specVersion, '2026-07-28'); const initialScope = 'mcp:basic'; // tools/call gates on mcp:write only (not the union) so the scenario can diff --git a/src/scenarios/client/auth/test_helpers/testClient.ts b/src/scenarios/client/auth/test_helpers/testClient.ts index e21459a7..a7d3b8bf 100644 --- a/src/scenarios/client/auth/test_helpers/testClient.ts +++ b/src/scenarios/client/auth/test_helpers/testClient.ts @@ -1,6 +1,7 @@ import { getScenario } from '../../../index'; import { testScenarioContext } from '../../../../mock-server/testing'; import type { SpecVersion, ConformanceCheck } from '../../../../types'; +import { protocolVersionFor } from '../../../../types'; import { spawn } from 'child_process'; const CLIENT_TIMEOUT = 10000; // 10 seconds for client to complete @@ -96,9 +97,9 @@ export interface RunClientOptions { expectedSuccessSlugs?: string[]; allowClientError?: boolean; /** - * Spec version to run the scenario at. Defaults to the latest dated spec - * version (see {@link testScenarioContext}). Pass the draft version to - * exercise checks gated to draft-only requirements. + * Spec version to run the scenario at. Defaults to the last stateful + * revision (see {@link testScenarioContext}). Pass `'2026-07-28'` to + * exercise checks gated to that revision, or the draft for draft-only ones. */ specVersion?: SpecVersion; } @@ -135,7 +136,9 @@ export async function runClientAgainstScenario( // Set environment variables for inline clients // These mirror what src/runner/client.ts does for spawned processes process.env.MCP_CONFORMANCE_SCENARIO = scenarioName; - process.env.MCP_CONFORMANCE_PROTOCOL_VERSION = ctx.specVersion; + process.env.MCP_CONFORMANCE_PROTOCOL_VERSION = protocolVersionFor( + ctx.specVersion + ); if (urls.context) { process.env.MCP_CONFORMANCE_CONTEXT = JSON.stringify({ name: scenarioName, diff --git a/src/scenarios/client/elicitation-defaults.ts b/src/scenarios/client/elicitation-defaults.ts index d4170295..6728bedc 100644 --- a/src/scenarios/client/elicitation-defaults.ts +++ b/src/scenarios/client/elicitation-defaults.ts @@ -14,7 +14,7 @@ import { } from '@modelcontextprotocol/sdk/types.js'; import type { Scenario, ConformanceCheck } from '../../types'; import express, { Request, Response } from 'express'; -import { ScenarioUrls, DRAFT_PROTOCOL_VERSION } from '../../types'; +import { ScenarioUrls } from '../../types'; import { createRequestLogger } from '../request-logger'; import { randomUUID } from 'crypto'; @@ -477,7 +477,7 @@ export class ElicitationClientDefaultsScenario implements Scenario { name = 'elicitation-sep1034-client-defaults'; readonly source = { introducedIn: '2025-11-25', - removedIn: DRAFT_PROTOCOL_VERSION + removedIn: '2026-07-28' } as const; description = 'Tests client applies default values for omitted elicitation fields (SEP-1034)'; diff --git a/src/scenarios/client/http-base.ts b/src/scenarios/client/http-base.ts index 6751dedd..0b26393f 100644 --- a/src/scenarios/client/http-base.ts +++ b/src/scenarios/client/http-base.ts @@ -1,5 +1,5 @@ import { - withRequiredDraftResultFields, + withRequiredResultFields, type ScenarioContext } from '../../mock-server'; /** @@ -17,7 +17,9 @@ import { ScenarioUrls, ConformanceCheck, ScenarioSource, - DRAFT_PROTOCOL_VERSION + SpecVersion, + LATEST_SPEC_VERSION, + protocolVersionFor } from '../../types.js'; /** @@ -28,8 +30,8 @@ import { * scenario does not route still carries its required list member — a bare * `{}` fails schema validation and strict clients drop the connection before * the scenario's real checks run (#474). `tasks/list` exists only at - * 2025-11-25 (the draft schema has no ListTasksResult); the empty member is - * harmless on the draft wire. Non-list results (tools/call, resources/read, + * 2025-11-25 (the 2026-07-28 schema has no ListTasksResult); the empty member + * is harmless on the stateless wire. Non-list results (tools/call, resources/read, * prompts/get, ...) have no meaningful empty default and keep the bare * stamped fallback, so a route a scenario forgot surfaces instead of being * masked. @@ -46,15 +48,18 @@ const EMPTY_LIST_RESULTS: ReadonlyMap = new Map([ export abstract class BaseHttpScenario implements Scenario { abstract name: string; abstract description: string; - readonly source: ScenarioSource = { introducedIn: DRAFT_PROTOCOL_VERSION }; + readonly source: ScenarioSource = { introducedIn: '2026-07-28' }; allowClientError?: boolean; protected server: http.Server | null = null; protected checks: ConformanceCheck[] = []; protected port: number = 0; protected sessionId: string = `session-${Date.now()}`; + /** Spec version of the current run; the mock advertises its wire version. */ + protected specVersion: SpecVersion = LATEST_SPEC_VERSION; - async start(_ctx: ScenarioContext): Promise { + async start(ctx: ScenarioContext): Promise { + this.specVersion = ctx.specVersion; return new Promise((resolve, reject) => { this.server = http.createServer((req, res) => { this.handleRequest(req, res); @@ -169,8 +174,8 @@ export abstract class BaseHttpScenario implements Scenario { this.sendJson(res, { jsonrpc: '2.0', id: request.id, - result: withRequiredDraftResultFields('server/discover', { - supportedVersions: [DRAFT_PROTOCOL_VERSION], + result: withRequiredResultFields('server/discover', { + supportedVersions: [protocolVersionFor(this.specVersion)], capabilities: this.discoverCapabilities(), serverInfo: { name: this.name + '-server', version: '1.0.0' } }) @@ -187,7 +192,7 @@ export abstract class BaseHttpScenario implements Scenario { id: request.id, result: { resultType: 'complete', - protocolVersion: DRAFT_PROTOCOL_VERSION, + protocolVersion: protocolVersionFor(this.specVersion), serverInfo: { name: this.name + '-server', version: '1.0.0' }, capabilities } @@ -204,9 +209,9 @@ export abstract class BaseHttpScenario implements Scenario { jsonrpc: '2.0', id: request.id, // Method-aware so cacheable methods that fall through to the generic - // reply still carry the ttlMs/cacheScope the draft revision requires, + // reply still carry the ttlMs/cacheScope the 2026-07-28 revision requires, // and unrouted standard list methods carry their required list member. - result: withRequiredDraftResultFields( + result: withRequiredResultFields( request.method, EMPTY_LIST_RESULTS.get(request.method) ?? {} ) diff --git a/src/scenarios/client/initialize.ts b/src/scenarios/client/initialize.ts index be8d7082..f009bcf4 100644 --- a/src/scenarios/client/initialize.ts +++ b/src/scenarios/client/initialize.ts @@ -5,8 +5,7 @@ import { ScenarioUrls, ConformanceCheck, LATEST_SPEC_VERSION, - NEGOTIABLE_PROTOCOL_VERSIONS, - DRAFT_PROTOCOL_VERSION + NEGOTIABLE_PROTOCOL_VERSIONS } from '../../types'; import { clientChecks } from '../../checks/index'; @@ -14,7 +13,7 @@ export class InitializeScenario implements Scenario { name = 'initialize'; readonly source = { introducedIn: '2025-06-18', - removedIn: DRAFT_PROTOCOL_VERSION + removedIn: '2026-07-28' } as const; description = 'Tests MCP client initialization handshake'; diff --git a/src/scenarios/client/json-schema-2020-12-preservation.test.ts b/src/scenarios/client/json-schema-2020-12-preservation.test.ts index ca803466..bb335676 100644 --- a/src/scenarios/client/json-schema-2020-12-preservation.test.ts +++ b/src/scenarios/client/json-schema-2020-12-preservation.test.ts @@ -4,9 +4,14 @@ import { Client } from '@modelcontextprotocol/sdk/client/index.js'; import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'; import { JsonSchema2020_12PreservationScenario } from './json-schema-2020-12-preservation'; import { sendStatelessRequest } from '../../connection/stateless'; -import { DRAFT_PROTOCOL_VERSION, LATEST_SPEC_VERSION } from '../../types'; import { JSON_SCHEMA_2020_12_FIXTURE } from '../server/json-schema-2020-12'; +// The SDK Client below drives the stateful lifecycle (last stateful +// revision); the raw helper drives the stateless one (first stateless +// revision, where SEP-2106 applies). +const STATEFUL = '2025-11-25' as const; +const STATELESS = '2026-07-28' as const; + const FOCAL_TOOL = 'json_schema_2020_12_tool'; const ECHO_TOOL = 'json_schema_echo'; @@ -46,7 +51,7 @@ async function runEchoClient( /** * Drive the scenario's mock server through the SEP-2575 stateless lifecycle - * that the draft protocol version uses: list tools, then echo the focal + * that 2026-07-28 onward uses: list tools, then echo the focal * tool's inputSchema back (verbatim or after the caller-supplied transform). * The SDK `Client` does not support the stateless lifecycle, so these * requests go through the raw stateless helper instead. @@ -104,11 +109,9 @@ describe('json-schema-2020-12-preservation scenario', () => { } }); - it('emits SUCCESS for SEP-1613 keywords and SKIPPED for SEP-2106 when a compliant client echoes back on a dated target', async () => { + it('emits SUCCESS for SEP-1613 keywords and SKIPPED for SEP-2106 when a compliant client echoes back on a pre-2026-07-28 target', async () => { const scenario = new JsonSchema2020_12PreservationScenario(); - const { serverUrl } = await scenario.start( - testScenarioContext(LATEST_SPEC_VERSION) - ); + const { serverUrl } = await scenario.start(testScenarioContext(STATEFUL)); try { await runEchoClient(serverUrl); @@ -151,11 +154,9 @@ describe('json-schema-2020-12-preservation scenario', () => { } }); - it('emits all SUCCESS when a compliant client echoes back on the draft target', async () => { + it('emits all SUCCESS when a compliant client echoes back on the 2026-07-28 target', async () => { const scenario = new JsonSchema2020_12PreservationScenario(); - const { serverUrl } = await scenario.start( - testScenarioContext(DRAFT_PROTOCOL_VERSION) - ); + const { serverUrl } = await scenario.start(testScenarioContext(STATELESS)); try { await runStatelessEchoClient(serverUrl); @@ -170,9 +171,7 @@ describe('json-schema-2020-12-preservation scenario', () => { it('flags SEP-1613 FAILURE when a client strips $schema and $defs before echoing', async () => { const scenario = new JsonSchema2020_12PreservationScenario(); - const { serverUrl } = await scenario.start( - testScenarioContext(LATEST_SPEC_VERSION) - ); + const { serverUrl } = await scenario.start(testScenarioContext(STATEFUL)); try { await runEchoClient(serverUrl, (schema) => { const stripped = { ...schema }; @@ -211,11 +210,9 @@ describe('json-schema-2020-12-preservation scenario', () => { } }); - it('flags SEP-2106 FAILURE on the draft target when composition keywords are stripped', async () => { + it('flags SEP-2106 FAILURE on the 2026-07-28 target when composition keywords are stripped', async () => { const scenario = new JsonSchema2020_12PreservationScenario(); - const { serverUrl } = await scenario.start( - testScenarioContext(DRAFT_PROTOCOL_VERSION) - ); + const { serverUrl } = await scenario.start(testScenarioContext(STATELESS)); try { await runStatelessEchoClient(serverUrl, (schema) => { const stripped = { ...schema }; diff --git a/src/scenarios/client/json-schema-2020-12-preservation.ts b/src/scenarios/client/json-schema-2020-12-preservation.ts index 15185958..2ad0dee2 100644 --- a/src/scenarios/client/json-schema-2020-12-preservation.ts +++ b/src/scenarios/client/json-schema-2020-12-preservation.ts @@ -21,8 +21,8 @@ * flags any missing or altered keywords. * * SEP-2106 vocabulary checks are soft-gated via `sep2106KeywordCheckStatus`: - * stripping is FAILURE only when the run targets the draft version - * (`2026-07-28`); on earlier dated versions the check reports SKIPPED. + * stripping is FAILURE only when the run targets `2026-07-28` or later; on + * earlier dated versions the check reports SKIPPED. */ import type { ScenarioContext, MockServer } from '../../mock-server'; @@ -31,7 +31,6 @@ import type { ListToolsResult } from '../../spec-types/2025-11-25'; import { - DRAFT_PROTOCOL_VERSION, LATEST_SPEC_VERSION, type ConformanceCheck, type Scenario, @@ -42,6 +41,7 @@ import { EXPECTED_SCHEMA_DIALECT, EXPECTED_TOOL_NAME, JSON_SCHEMA_2020_12_FIXTURE, + SEP_2106_SINCE, sep2106KeywordCheckStatus } from '../server/json-schema-2020-12'; @@ -77,7 +77,7 @@ export class JsonSchema2020_12PreservationScenario implements Scenario { The scenario compares the echoed schema against the original fixture and flags any keyword that was stripped or altered during the client's internal parsing. -**Verification**: \`$schema\`, \`$defs\`, and \`additionalProperties\` must be preserved (SEP-1613). The SEP-2106 vocabulary (\`$anchor\` inside \`$defs\`, composition \`allOf\`/\`anyOf\`, conditional \`if\`/\`then\`/\`else\`) must also survive; for dated protocol versions these checks are SKIPPED rather than FAILURE since SEP-2106 applies from the draft version.`; +**Verification**: \`$schema\`, \`$defs\`, and \`additionalProperties\` must be preserved (SEP-1613). The SEP-2106 vocabulary (\`$anchor\` inside \`$defs\`, composition \`allOf\`/\`anyOf\`, conditional \`if\`/\`then\`/\`else\`) must also survive; for protocol versions before 2026-07-28 these checks are SKIPPED rather than FAILURE since SEP-2106 applies from that revision.`; private srv: MockServer | null = null; private specVersion: SpecVersion | null = null; @@ -309,7 +309,7 @@ The scenario compares the echoed schema against the original fixture and flags a }); // SEP-2106 vocabulary — soft-gated via sep2106KeywordCheckStatus. - const skippedSuffix = ` (run targets protocol version ${targetVersion}; SEP-2106 applies from ${DRAFT_PROTOCOL_VERSION})`; + const skippedSuffix = ` (run targets protocol version ${targetVersion}; SEP-2106 applies from ${SEP_2106_SINCE})`; // SEP-2106: composition (allOf/anyOf) preserved const allOf = echo['allOf']; diff --git a/src/scenarios/client/json-schema-ref-deref.test.ts b/src/scenarios/client/json-schema-ref-deref.test.ts index 8445f1b4..89e822b0 100644 --- a/src/scenarios/client/json-schema-ref-deref.test.ts +++ b/src/scenarios/client/json-schema-ref-deref.test.ts @@ -9,7 +9,8 @@ import { import { JsonSchemaRefDerefScenario } from './json-schema-ref-deref'; import { getScenario } from '../index'; import { sendStatelessRequest } from '../../connection/stateless'; -import { DRAFT_PROTOCOL_VERSION } from '../../types'; +// Stateless-lifecycle tests pin the first stateless release. +const STATELESS = '2026-07-28' as const; /** * SEP-2106: implementations MUST NOT automatically dereference $ref values @@ -77,27 +78,29 @@ async function dereferencingClient(serverUrl: string): Promise { * SEP-2575 stateless client: probe `server/discover` first, then honor the * negotiated protocol version on subsequent requests. Regression client for * issue #397 — the scenario's hand-rolled `server/discover` advertises the - * draft version, so the pinned SDK transport must not reject that version's - * MCP-Protocol-Version header on the follow-up tools/list. + * run's (stateless) version, so the pinned SDK transport must not reject that + * version's MCP-Protocol-Version header on the follow-up tools/list. */ -async function statelessDraftClient(serverUrl: string): Promise { +async function statelessClient(serverUrl: string): Promise { const discover = await sendStatelessRequest(serverUrl, 'server/discover'); const supportedVersions = ( discover.body?.result as { supportedVersions?: string[] } | undefined )?.supportedVersions; - if (!supportedVersions?.includes(DRAFT_PROTOCOL_VERSION)) { + if (!supportedVersions?.includes(STATELESS)) { throw new Error( - `server/discover did not advertise ${DRAFT_PROTOCOL_VERSION}: ` + + `server/discover did not advertise ${STATELESS}: ` + JSON.stringify(supportedVersions) ); } - // sendStatelessRequest sends MCP-Protocol-Version: by default — - // exactly what a client that honors the negotiated version would do. - const tools = await sendStatelessRequest(serverUrl, 'tools/list'); + // Send MCP-Protocol-Version: — exactly what a client that + // honors the negotiated version would do. + const tools = await sendStatelessRequest(serverUrl, 'tools/list', undefined, { + specVersion: STATELESS + }); if (tools.status !== 200 || tools.body?.result === undefined) { throw new Error( - `tools/list with the negotiated draft version failed: HTTP ${tools.status} ` + + `tools/list with the negotiated version failed: HTTP ${tools.status} ` + JSON.stringify(tools.body ?? tools.text) ); } @@ -115,10 +118,11 @@ describe('json-schema-ref-no-deref (SEP-2106)', () => { ); }); - test('stateless draft client passes: negotiated version reaches tools/list (issue #397)', async () => { + test('stateless client passes: negotiated version reaches tools/list (issue #397)', async () => { await runClientAgainstScenario( - new InlineClientRunner(statelessDraftClient), - 'json-schema-ref-no-deref' + new InlineClientRunner(statelessClient), + 'json-schema-ref-no-deref', + { specVersion: STATELESS } ); }); diff --git a/src/scenarios/client/json-schema-ref-deref.ts b/src/scenarios/client/json-schema-ref-deref.ts index aff791d3..75cf1822 100644 --- a/src/scenarios/client/json-schema-ref-deref.ts +++ b/src/scenarios/client/json-schema-ref-deref.ts @@ -7,7 +7,7 @@ import { } from '@modelcontextprotocol/sdk/types.js'; import type { Scenario, ConformanceCheck } from '../../types'; import express, { Request, Response } from 'express'; -import { ScenarioUrls, DRAFT_PROTOCOL_VERSION } from '../../types'; +import { ScenarioUrls, protocolVersionFor } from '../../types'; /** * Scenario: JSON Schema network $ref dereferencing (SEP-2106) @@ -79,7 +79,7 @@ function createMcpServer(canaryUrl: string, onToolsListed: () => void): Server { export class JsonSchemaRefDerefScenario implements Scenario { name = 'json-schema-ref-no-deref'; - readonly source = { introducedIn: DRAFT_PROTOCOL_VERSION } as const; + readonly source = { introducedIn: '2026-07-28' } as const; description = `Tests that a client does not automatically dereference a network-URI \`$ref\` in a tool's inputSchema (SEP-2106). The scenario advertises a tool whose inputSchema contains a \`$ref\` pointing at a canary URL. The client should list tools (and may otherwise process the schema), but must not fetch the canary URL. Same-document refs (\`#/$defs/...\`) remain safe to resolve.`; @@ -89,9 +89,10 @@ The scenario advertises a tool whose inputSchema contains a \`$ref\` pointing at private canaryRequests: Array<{ method: string; userAgent?: string }> = []; private toolsListed = false; - async start(_ctx: ScenarioContext): Promise { + async start(ctx: ScenarioContext): Promise { this.canaryRequests = []; this.toolsListed = false; + const protocolVersion = protocolVersionFor(ctx.specVersion); const app = express(); app.use(express.json()); @@ -126,7 +127,7 @@ The scenario advertises a tool whose inputSchema contains a \`$ref\` pointing at resultType: 'complete', ttlMs: 0, cacheScope: 'private', - supportedVersions: [DRAFT_PROTOCOL_VERSION], + supportedVersions: [protocolVersion], capabilities: { tools: {} }, serverInfo: { name: 'json-schema-ref-deref-server', @@ -136,11 +137,11 @@ The scenario advertises a tool whose inputSchema contains a \`$ref\` pointing at }); } // Second half of the same workaround: the pinned SDK transport - // whitelists MCP-Protocol-Version headers and would reject the draft + // whitelists MCP-Protocol-Version headers and would reject the // version that the server/discover response above advertises with an // HTTP 400. Rewrite it to the newest version the SDK understands so a // client that honors the negotiated version can reach tools/list. - if (req.headers['mcp-protocol-version'] === DRAFT_PROTOCOL_VERSION) { + if (req.headers['mcp-protocol-version'] === protocolVersion) { req.headers['mcp-protocol-version'] = SDK_LATEST_PROTOCOL_VERSION; // The SDK's Node adapter rebuilds its web-standard Request from // rawHeaders, not the parsed headers object, so patch those too. diff --git a/src/scenarios/client/mrtr-client.ts b/src/scenarios/client/mrtr-client.ts index 2bd5692e..d592a81f 100644 --- a/src/scenarios/client/mrtr-client.ts +++ b/src/scenarios/client/mrtr-client.ts @@ -12,7 +12,7 @@ import type { ScenarioContext } from '../../mock-server'; */ import type { Scenario, ConformanceCheck } from '../../types'; -import { DRAFT_PROTOCOL_VERSION, ScenarioUrls } from '../../types'; +import { ScenarioUrls, protocolVersionFor } from '../../types'; import express, { Request, Response } from 'express'; import { randomUUID } from 'crypto'; @@ -73,7 +73,10 @@ interface JsonRpcRequest { params?: Record; } -function createMRTRServer(checks: ConformanceCheck[]): express.Application { +function createMRTRServer( + checks: ConformanceCheck[], + protocolVersion: string +): express.Application { const app = express(); app.use(express.json()); @@ -95,7 +98,7 @@ function createMRTRServer(checks: ConformanceCheck[]): express.Application { resultType: 'complete', ttlMs: 0, cacheScope: 'private', - supportedVersions: [DRAFT_PROTOCOL_VERSION], + supportedVersions: [protocolVersion], capabilities: { tools: {} }, serverInfo: { name: 'mrtr-mock-server', version: '1.0.0' } } @@ -465,16 +468,19 @@ function createMRTRServer(checks: ConformanceCheck[]): express.Application { export class MRTRClientScenario implements Scenario { name = 'sep-2322-client-request-state'; - readonly source = { introducedIn: DRAFT_PROTOCOL_VERSION } as const; + readonly source = { introducedIn: '2026-07-28' } as const; description = 'Tests client MRTR behavior: requestState echo, no-state omission, and JSON-RPC id uniqueness (SEP-2322)'; private app: express.Application | null = null; private httpServer: ReturnType | null = null; private checks: ConformanceCheck[] = []; - async start(_ctx: ScenarioContext): Promise { + async start(ctx: ScenarioContext): Promise { this.checks = []; - this.app = createMRTRServer(this.checks); + this.app = createMRTRServer( + this.checks, + protocolVersionFor(ctx.specVersion) + ); this.httpServer = this.app.listen(0); const addr = this.httpServer.address(); const port = typeof addr === 'object' && addr ? addr.port : 0; diff --git a/src/scenarios/client/request-metadata.test.ts b/src/scenarios/client/request-metadata.test.ts index 0227d3f8..93e6641b 100644 --- a/src/scenarios/client/request-metadata.test.ts +++ b/src/scenarios/client/request-metadata.test.ts @@ -11,6 +11,10 @@ import { RequestMetadataScenario } from './request-metadata'; +// request-metadata is a 2026-07-28 scenario; drive it at that revision so the +// mock advertises (and the clients below send) the matching wire version. +const STATELESS = '2026-07-28' as const; + // A bad client that does not send _meta async function badClient(serverUrl: string) { const response = await fetch(serverUrl, { @@ -29,7 +33,7 @@ async function badClient(serverUrl: string) { } const goodMeta = { - 'io.modelcontextprotocol/protocolVersion': '2026-07-28', + 'io.modelcontextprotocol/protocolVersion': STATELESS, 'io.modelcontextprotocol/clientInfo': { name: 'test', version: '1.0' }, 'io.modelcontextprotocol/clientCapabilities': {} }; @@ -155,7 +159,9 @@ describe('request-metadata client scenario — positive test', () => { } const runner = new InlineClientRunner(clientFn); - await runClientAgainstScenario(runner, 'request-metadata'); + await runClientAgainstScenario(runner, 'request-metadata', { + specVersion: STATELESS + }); // Extract checks directly from the scenario instance const checks = scenario.getChecks(); @@ -210,7 +216,7 @@ describe('request-metadata client scenario — client never connects', () => { throw new Error('Scenario not found'); } - await scenario.start(testScenarioContext()); + await scenario.start(testScenarioContext(STATELESS)); try { const checks = scenario.getChecks(); const byId = new Map(checks.map((c) => [c.id, c])); @@ -230,6 +236,7 @@ describe('request-metadata client scenario — client never connects', () => { test('does not overwrite checks recorded from a real request', async () => { const runner = new InlineClientRunner(badClient); await runClientAgainstScenario(runner, 'request-metadata', { + specVersion: STATELESS, expectedFailureSlugs: [ 'sep-2575-client-populates-meta', 'sep-2575-http-client-sends-version-header' @@ -254,7 +261,9 @@ describe('request-metadata client scenario — HTTP handling', () => { 'rejects an empty-body %s request without parsing it as JSON', async (method) => { const scenario = new RequestMetadataScenario(); - const { serverUrl } = await scenario.start(testScenarioContext()); + const { serverUrl } = await scenario.start( + testScenarioContext(STATELESS) + ); try { const response = await fetch(serverUrl, { method }); @@ -270,7 +279,7 @@ describe('request-metadata client scenario — HTTP handling', () => { test('returns a JSON-RPC parse error for malformed POST bodies', async () => { const scenario = new RequestMetadataScenario(); - const { serverUrl } = await scenario.start(testScenarioContext()); + const { serverUrl } = await scenario.start(testScenarioContext(STATELESS)); try { const response = await fetch(serverUrl, { @@ -295,6 +304,7 @@ describe('request-metadata client scenario — negative tests', () => { test('client fails when omitting _meta', async () => { const runner = new InlineClientRunner(badClient); await runClientAgainstScenario(runner, 'request-metadata', { + specVersion: STATELESS, expectedFailureSlugs: [ 'sep-2575-client-populates-meta', 'sep-2575-http-client-sends-version-header' @@ -305,6 +315,7 @@ describe('request-metadata client scenario — negative tests', () => { test('client fails when missing version header', async () => { const runner = new InlineClientRunner(missingHeaderClient); await runClientAgainstScenario(runner, 'request-metadata', { + specVersion: STATELESS, expectedFailureSlugs: ['sep-2575-http-client-sends-version-header'] }); }); @@ -312,6 +323,7 @@ describe('request-metadata client scenario — negative tests', () => { test('client fails when header disagrees with _meta', async () => { const runner = new InlineClientRunner(mismatchedHeaderClient); await runClientAgainstScenario(runner, 'request-metadata', { + specVersion: STATELESS, expectedFailureSlugs: ['sep-2575-http-version-header-matches-meta'] }); }); @@ -319,6 +331,7 @@ describe('request-metadata client scenario — negative tests', () => { test('client fails retry check when it does not handle 400 rejection', async () => { const runner = new InlineClientRunner(nonRetryingClient); await runClientAgainstScenario(runner, 'request-metadata', { + specVersion: STATELESS, expectedFailureSlugs: ['sep-2575-client-retry-supported-version'] }); }); @@ -326,6 +339,7 @@ describe('request-metadata client scenario — negative tests', () => { test('client aborts cleanly without hanging when negotiation has empty version intersection', async () => { const runner = new InlineClientRunner(incompatibleVersionClient); await runClientAgainstScenario(runner, 'request-metadata', { + specVersion: STATELESS, expectedFailureSlugs: ['sep-2575-client-retry-supported-version'] }); }); @@ -333,6 +347,7 @@ describe('request-metadata client scenario — negative tests', () => { test('client triggers failures for malformed capabilities', async () => { const runner = new InlineClientRunner(malformedCapabilitiesClient); await runClientAgainstScenario(runner, 'request-metadata', { + specVersion: STATELESS, expectedFailureSlugs: [ 'sep-2575-client-declares-roots-capability', 'sep-2575-client-declares-elicitation-capability' diff --git a/src/scenarios/client/request-metadata.ts b/src/scenarios/client/request-metadata.ts index 54673b00..6b62a9a7 100644 --- a/src/scenarios/client/request-metadata.ts +++ b/src/scenarios/client/request-metadata.ts @@ -1,5 +1,5 @@ import { - withRequiredDraftResultFields, + withRequiredResultFields, type ScenarioContext } from '../../mock-server'; import http from 'http'; @@ -8,7 +8,8 @@ import { ScenarioUrls, ConformanceCheck, CheckStatus, - DRAFT_PROTOCOL_VERSION + LATEST_SPEC_VERSION, + protocolVersionFor } from '../../types'; /** @@ -42,7 +43,7 @@ export const DECLARED_CHECK_IDS = [ export class RequestMetadataScenario implements Scenario { name = 'request-metadata'; - readonly source = { introducedIn: DRAFT_PROTOCOL_VERSION } as const; + readonly source = { introducedIn: '2026-07-28' } as const; description = 'Per-request _meta and MCP-Protocol-Version header obligations (SEP-2575)'; @@ -50,11 +51,14 @@ export class RequestMetadataScenario implements Scenario { private checks: ConformanceCheck[] = []; private hasSimulatedRejection = false; private requestsObserved = 0; + /** Wire protocolVersion the mock supports: the run's spec version. */ + private protocolVersion: string = protocolVersionFor(LATEST_SPEC_VERSION); - async start(_ctx: ScenarioContext): Promise { + async start(ctx: ScenarioContext): Promise { this.hasSimulatedRejection = false; this.checks = []; this.requestsObserved = 0; + this.protocolVersion = protocolVersionFor(ctx.specVersion); return new Promise((resolve, reject) => { this.server = http.createServer((req, res) => { this.handleRequest(req, res); @@ -329,11 +333,11 @@ export class RequestMetadataScenario implements Scenario { jsonrpc: '2.0', id: request.id ?? null, error: { - // UnsupportedProtocolVersionError per the draft schema. + // UnsupportedProtocolVersionError per the 2026-07-28 schema. code: -32022, message: 'Unsupported protocol version', data: { - supported: [DRAFT_PROTOCOL_VERSION], + supported: [this.protocolVersion], requested: String(headerVersion ?? metaVersion ?? '') } } @@ -347,8 +351,8 @@ export class RequestMetadataScenario implements Scenario { ); if (retryCheck) { if ( - headerVersion === DRAFT_PROTOCOL_VERSION && - metaVersion === DRAFT_PROTOCOL_VERSION + headerVersion === this.protocolVersion && + metaVersion === this.protocolVersion ) { retryCheck.status = 'SUCCESS'; } else { @@ -370,8 +374,8 @@ export class RequestMetadataScenario implements Scenario { JSON.stringify({ jsonrpc: '2.0', id: request.id, - result: withRequiredDraftResultFields(request.method, { - supportedVersions: [DRAFT_PROTOCOL_VERSION], + result: withRequiredResultFields(request.method, { + supportedVersions: [this.protocolVersion], capabilities: {}, serverInfo: { name: 'test', version: '1.0' } }) @@ -392,7 +396,7 @@ export class RequestMetadataScenario implements Scenario { JSON.stringify({ jsonrpc: '2.0', id: request.id, - result: withRequiredDraftResultFields(request.method, result) + result: withRequiredResultFields(request.method, result) }) ); }); diff --git a/src/scenarios/client/draft-result-fields.test.ts b/src/scenarios/client/required-result-fields.test.ts similarity index 87% rename from src/scenarios/client/draft-result-fields.test.ts rename to src/scenarios/client/required-result-fields.test.ts index 27f64de5..e0fe5641 100644 --- a/src/scenarios/client/draft-result-fields.test.ts +++ b/src/scenarios/client/required-result-fields.test.ts @@ -1,8 +1,9 @@ import { describe, it, expect } from 'vitest'; import { testScenarioContext } from '../../mock-server/testing'; -import { withRequiredDraftResultFields } from '../../mock-server'; +import { withRequiredResultFields } from '../../mock-server'; import { wireSchemaErrors } from '../../validation/wire-schema'; -import { DRAFT_PROTOCOL_VERSION } from '../../types'; +// Stateless-lifecycle tests pin the first stateless release. +const STATELESS = '2026-07-28' as const; import { HttpStandardHeadersScenario } from './http-standard-headers'; import { HttpCustomHeadersScenario, @@ -23,7 +24,7 @@ import { JsonSchemaRefDerefScenario } from './json-schema-ref-deref'; */ const meta = { - 'io.modelcontextprotocol/protocolVersion': DRAFT_PROTOCOL_VERSION, + 'io.modelcontextprotocol/protocolVersion': STATELESS, 'io.modelcontextprotocol/clientInfo': { name: 'test', version: '1.0' }, 'io.modelcontextprotocol/clientCapabilities': {} }; @@ -49,7 +50,7 @@ const CACHEABLE_FIELDS = { const DISCOVER_FIELDS = { ...CACHEABLE_FIELDS, - supportedVersions: [DRAFT_PROTOCOL_VERSION] + supportedVersions: [STATELESS] }; describe('hand-rolled mock servers serve server/discover (2026-07-28)', () => { @@ -85,7 +86,7 @@ describe('hand-rolled mock servers serve server/discover (2026-07-28)', () => { it(`${c.name} returns a valid DiscoverResult`, async () => { const scenario = c.make(); const { serverUrl } = await scenario.start( - testScenarioContext(DRAFT_PROTOCOL_VERSION) + testScenarioContext(STATELESS) ); try { const { status, body } = await post( @@ -96,7 +97,7 @@ describe('hand-rolled mock servers serve server/discover (2026-07-28)', () => { method: 'server/discover', params: { _meta: meta } }, - { 'mcp-protocol-version': DRAFT_PROTOCOL_VERSION } + { 'mcp-protocol-version': STATELESS } ); expect(status).toBe(200); expect(body.result).toMatchObject({ @@ -112,11 +113,9 @@ describe('hand-rolled mock servers serve server/discover (2026-07-28)', () => { }); describe('http-standard-headers mock results (2026-07-28)', () => { - it('carries the draft-required result members on every handled method', async () => { + it('carries the 2026-07-28-required result members on every handled method', async () => { const scenario = new HttpStandardHeadersScenario(); - const { serverUrl } = await scenario.start( - testScenarioContext(DRAFT_PROTOCOL_VERSION) - ); + const { serverUrl } = await scenario.start(testScenarioContext(STATELESS)); try { const cases: Array<{ method: string; @@ -184,9 +183,7 @@ describe('generic fallback answers unrouted list methods schema-valid (#474)', ( // scenario's header checks ran. it('http-standard-headers answers resources/templates/list with a valid empty result', async () => { const scenario = new HttpStandardHeadersScenario(); - const { serverUrl } = await scenario.start( - testScenarioContext(DRAFT_PROTOCOL_VERSION) - ); + const { serverUrl } = await scenario.start(testScenarioContext(STATELESS)); try { const { status, body } = await post( serverUrl, @@ -204,11 +201,7 @@ describe('generic fallback answers unrouted list methods schema-valid (#474)', ( resourceTemplates: [] }); expect( - wireSchemaErrors( - DRAFT_PROTOCOL_VERSION, - body, - 'resources/templates/list' - ) + wireSchemaErrors(STATELESS, body, 'resources/templates/list') ).toEqual([]); } finally { await scenario.stop(); @@ -254,7 +247,7 @@ describe('generic fallback answers unrouted list methods schema-valid (#474)', ( it(`${s.name} answers unrouted list methods with valid empty results`, async () => { const scenario = s.make(); const { serverUrl } = await scenario.start( - testScenarioContext(DRAFT_PROTOCOL_VERSION) + testScenarioContext(STATELESS) ); try { let id = 1; @@ -266,10 +259,7 @@ describe('generic fallback answers unrouted list methods schema-valid (#474)', ( ); expect(status, method).toBe(200); expect(body.result[listMember.get(method)!], method).toEqual([]); - expect( - wireSchemaErrors(DRAFT_PROTOCOL_VERSION, body, method), - method - ).toEqual([]); + expect(wireSchemaErrors(STATELESS, body, method), method).toEqual([]); if (method === 'tasks/list') { expect(wireSchemaErrors('2025-11-25', body, method)).toEqual([]); } @@ -288,7 +278,7 @@ describe('generic fallback answers unrouted list methods schema-valid (#474)', ( it(`${s.name} answers Object.prototype-colliding method names with a valid generic result`, async () => { const scenario = s.make(); const { serverUrl } = await scenario.start( - testScenarioContext(DRAFT_PROTOCOL_VERSION) + testScenarioContext(STATELESS) ); try { let id = 1; @@ -305,10 +295,7 @@ describe('generic fallback answers unrouted list methods schema-valid (#474)', ( ); expect(status, method).toBe(200); expect(body.result, method).toEqual({ resultType: 'complete' }); - expect( - wireSchemaErrors(DRAFT_PROTOCOL_VERSION, body, method), - method - ).toEqual([]); + expect(wireSchemaErrors(STATELESS, body, method), method).toEqual([]); } } finally { await scenario.stop(); @@ -323,10 +310,10 @@ describe('generic fallback answers unrouted list methods schema-valid (#474)', ( const preFix = { jsonrpc: '2.0', id: 1, - result: withRequiredDraftResultFields('resources/templates/list', {}) + result: withRequiredResultFields('resources/templates/list', {}) }; const errors = wireSchemaErrors( - DRAFT_PROTOCOL_VERSION, + STATELESS, preFix, 'resources/templates/list' ); @@ -337,11 +324,9 @@ describe('generic fallback answers unrouted list methods schema-valid (#474)', ( }); describe('http-custom-headers mock results (2026-07-28)', () => { - it('carries the draft-required result members', async () => { + it('carries the 2026-07-28-required result members', async () => { const scenario = new HttpCustomHeadersScenario(); - const { serverUrl } = await scenario.start( - testScenarioContext(DRAFT_PROTOCOL_VERSION) - ); + const { serverUrl } = await scenario.start(testScenarioContext(STATELESS)); try { const list = await post(serverUrl, { jsonrpc: '2.0', @@ -368,11 +353,9 @@ describe('http-custom-headers mock results (2026-07-28)', () => { }); describe('http-invalid-tool-headers mock results (2026-07-28)', () => { - it('carries the draft-required result members', async () => { + it('carries the 2026-07-28-required result members', async () => { const scenario = new HttpInvalidToolHeadersScenario(); - const { serverUrl } = await scenario.start( - testScenarioContext(DRAFT_PROTOCOL_VERSION) - ); + const { serverUrl } = await scenario.start(testScenarioContext(STATELESS)); try { const list = await post(serverUrl, { jsonrpc: '2.0', @@ -396,12 +379,10 @@ describe('http-invalid-tool-headers mock results (2026-07-28)', () => { }); describe('request-metadata mock results (2026-07-28)', () => { - it('carries the draft-required result members after the simulated rejection', async () => { + it('carries the 2026-07-28-required result members after the simulated rejection', async () => { const scenario = new RequestMetadataScenario(); - const { serverUrl } = await scenario.start( - testScenarioContext(DRAFT_PROTOCOL_VERSION) - ); - const headers = { 'MCP-Protocol-Version': DRAFT_PROTOCOL_VERSION }; + const { serverUrl } = await scenario.start(testScenarioContext(STATELESS)); + const headers = { 'MCP-Protocol-Version': STATELESS }; try { // The first request is always answered with the simulated -32022 // rejection (retry probe); results are served from the second on. @@ -466,11 +447,9 @@ describe('request-metadata mock results (2026-07-28)', () => { }); describe('sep-2322-client-request-state mock results (2026-07-28)', () => { - it('carries the draft-required result members on conformant results and keeps the deliberate omission', async () => { + it('carries the 2026-07-28-required result members on conformant results and keeps the deliberate omission', async () => { const scenario = new MRTRClientScenario(); - const { serverUrl } = await scenario.start( - testScenarioContext(DRAFT_PROTOCOL_VERSION) - ); + const { serverUrl } = await scenario.start(testScenarioContext(STATELESS)); try { const list = await post(serverUrl, { jsonrpc: '2.0', diff --git a/src/scenarios/client/sse-retry.ts b/src/scenarios/client/sse-retry.ts index 3cee1e12..0aadbe5c 100644 --- a/src/scenarios/client/sse-retry.ts +++ b/src/scenarios/client/sse-retry.ts @@ -9,18 +9,13 @@ import type { ScenarioContext } from '../../mock-server'; */ import http from 'http'; -import { - Scenario, - ScenarioUrls, - ConformanceCheck, - DRAFT_PROTOCOL_VERSION -} from '../../types.js'; +import { Scenario, ScenarioUrls, ConformanceCheck } from '../../types.js'; export class SSERetryScenario implements Scenario { name = 'sse-retry'; readonly source = { introducedIn: '2025-11-25', - removedIn: DRAFT_PROTOCOL_VERSION + removedIn: '2026-07-28' } as const; description = 'Tests that client respects SSE retry field timing and reconnects properly (SEP-1699)'; diff --git a/src/scenarios/client/tools_call.test.ts b/src/scenarios/client/tools_call.test.ts index ad6a58e6..01fc65c5 100644 --- a/src/scenarios/client/tools_call.test.ts +++ b/src/scenarios/client/tools_call.test.ts @@ -3,7 +3,8 @@ import { describe, it, expect } from 'vitest'; import { Client } from '@modelcontextprotocol/sdk/client/index.js'; import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'; import { ToolsCallScenario } from './tools_call'; -import { DRAFT_PROTOCOL_VERSION } from '../../types'; +// Stateless-lifecycle tests pin the first stateless release. +const STATELESS = '2026-07-28' as const; describe('tools_call scenario', () => { it('emits a single FAILURE check when the tool was never called', async () => { @@ -21,9 +22,9 @@ describe('tools_call scenario', () => { } }); - it('serves spec-valid results at the draft (2026-07-28) version', async () => { + it('serves spec-valid results at the 2026-07-28 version', async () => { const meta = { - 'io.modelcontextprotocol/protocolVersion': DRAFT_PROTOCOL_VERSION, + 'io.modelcontextprotocol/protocolVersion': STATELESS, 'io.modelcontextprotocol/clientInfo': { name: 'test-client', version: '1.0.0' @@ -35,7 +36,7 @@ describe('tools_call scenario', () => { method: 'POST', headers: { 'content-type': 'application/json', - 'mcp-protocol-version': DRAFT_PROTOCOL_VERSION + 'mcp-protocol-version': STATELESS }, body: JSON.stringify(body) }); @@ -43,9 +44,7 @@ describe('tools_call scenario', () => { } const scenario = new ToolsCallScenario(); - const { serverUrl } = await scenario.start( - testScenarioContext(DRAFT_PROTOCOL_VERSION) - ); + const { serverUrl } = await scenario.start(testScenarioContext(STATELESS)); try { const list = await post(serverUrl, { jsonrpc: '2.0', diff --git a/src/scenarios/index.ts b/src/scenarios/index.ts index 03cf55f5..ab729bb5 100644 --- a/src/scenarios/index.ts +++ b/src/scenarios/index.ts @@ -4,10 +4,11 @@ import { ClientScenarioForAuthorizationServer, ScenarioSource, SpecVersion, - DatedSpecVersion, ScenarioSpecTag, - DATED_SPEC_VERSIONS, - DRAFT_PROTOCOL_VERSION + DRAFT_SPEC_VERSION, + DRAFT_PROTOCOL_VERSION, + LATEST_SPEC_VERSION, + SPEC_VERSION_TIMELINE } from '../types'; import { InitializeScenario } from './client/initialize'; import { ToolsCallScenario } from './client/tools_call'; @@ -106,7 +107,6 @@ import { import { authScenariosList, backcompatScenariosList, - draftScenariosList, extensionScenariosList } from './client/auth/index'; import { listMetadataScenarios } from './client/auth/discovery-metadata'; @@ -249,14 +249,16 @@ const allClientScenariosList: ClientScenario[] = [ new InputRequiredResultValidateInputScenario() ]; -// Scenarios that test requirements introduced in the in-progress draft spec. -// They run via `--suite draft` (or `--suite all`) and are excluded from the -// default `active` suite until the draft is published as a dated release. +// Scenarios that test requirements introduced in the in-progress draft spec +// (after the latest dated release). They run via `--suite draft` (or +// `--suite all`) and are excluded from the default `active` suite until the +// draft is published as a dated release and they are retagged to it. Empty +// whenever no scenario targets an unreleased requirement. const draftClientScenariosList: ClientScenario[] = allClientScenariosList.filter( (scenario) => 'introducedIn' in scenario.source && - scenario.source.introducedIn === DRAFT_PROTOCOL_VERSION + scenario.source.introducedIn === DRAFT_SPEC_VERSION ); // Active client scenarios (excludes pending and draft) @@ -302,7 +304,6 @@ const scenariosList: Scenario[] = [ new RequestMetadataScenario(), ...authScenariosList, ...backcompatScenariosList, - ...draftScenariosList, ...extensionScenariosList, // MRTR client conformance (SEP-2322) @@ -388,13 +389,14 @@ export function listClientScenariosForAuthorizationServer(): string[] { return Array.from(clientScenariosForAuthorizationServer.keys()); } -// All client-testing scenarios that target the draft spec, derived from the -// declared `source.introducedIn` rather than a hand-maintained list (covers -// both the auth draft scenarios and the non-auth ones, e.g. SEP-2243/2575). +// All client-testing scenarios that target the in-progress draft spec, +// derived from the declared `source.introducedIn` rather than a +// hand-maintained list. Empty whenever no scenario targets an unreleased +// requirement. const draftSpecScenariosList: Scenario[] = scenariosList.filter( (scenario) => 'introducedIn' in scenario.source && - scenario.source.introducedIn === DRAFT_PROTOCOL_VERSION + scenario.source.introducedIn === DRAFT_SPEC_VERSION ); export function listDraftScenarios(): string[] { @@ -410,29 +412,48 @@ export { listMetadataScenarios }; // All valid spec versions, used by the CLI to validate --spec-version input. // 'extension' is intentionally excluded — extension scenarios are off-timeline // and selected via `--suite extensions`, not `--spec-version`. -export const ALL_SPEC_VERSIONS: SpecVersion[] = [ - ...DATED_SPEC_VERSIONS, - DRAFT_PROTOCOL_VERSION -]; +export const ALL_SPEC_VERSIONS: readonly SpecVersion[] = SPEC_VERSION_TIMELINE; export function resolveSpecVersion(value: string): SpecVersion { - if (value === 'draft') return DRAFT_PROTOCOL_VERSION; if (ALL_SPEC_VERSIONS.includes(value as SpecVersion)) { + if (value === DRAFT_SPEC_VERSION) { + // One line, stderr: people who passed `draft` to reach the 2026-07-28 + // stateless era should know it is a dated release now. + console.error( + `Note: --spec-version draft targets the unreleased revision after ${LATEST_SPEC_VERSION} ` + + `(wire protocolVersion ${DRAFT_PROTOCOL_VERSION}). ${LATEST_SPEC_VERSION} is a released ` + + `revision; pass --spec-version ${LATEST_SPEC_VERSION} (or nothing) to target it.` + ); + } return value as SpecVersion; } console.error(`Unknown spec version: ${value}`); - console.error( - `Valid versions: ${ALL_SPEC_VERSIONS.join(', ')} (or 'draft' as an alias for ${DRAFT_PROTOCOL_VERSION})` - ); + console.error(`Valid versions: ${ALL_SPEC_VERSIONS.join(', ')}`); process.exit(1); } -function versionIndex( - v: DatedSpecVersion | typeof DRAFT_PROTOCOL_VERSION -): number { +function versionIndex(v: SpecVersion): number { return ALL_SPEC_VERSIONS.indexOf(v); } +/** + * The spec version a scenario runs at when `--spec-version` is omitted: the + * latest dated release if the scenario applies there, otherwise the newest + * revision inside its applicability window (the draft for draft-only + * scenarios, the last release before `removedIn` for removed ones). Keeps + * each scenario on a wire it was written for without the caller naming one. + */ +export function defaultSpecVersionFor(source: ScenarioSource): SpecVersion { + if ('extensionId' in source) return LATEST_SPEC_VERSION; + if (matchesSpecVersion(source, LATEST_SPEC_VERSION)) { + return LATEST_SPEC_VERSION; + } + const applicable = ALL_SPEC_VERSIONS.filter((v) => + matchesSpecVersion(source, v) + ); + return applicable[applicable.length - 1] ?? LATEST_SPEC_VERSION; +} + // Off-timeline sources (extensions etc.) are never selected by --spec-version. export function matchesSpecVersion( source: ScenarioSource, diff --git a/src/scenarios/server/all-scenarios.test.ts b/src/scenarios/server/all-scenarios.test.ts index bce7845f..37c4a663 100644 --- a/src/scenarios/server/all-scenarios.test.ts +++ b/src/scenarios/server/all-scenarios.test.ts @@ -2,16 +2,14 @@ import { testContext } from '../../connection/testing'; import { spawn, ChildProcess } from 'child_process'; import { createServer } from 'net'; import { + defaultSpecVersionFor, getClientScenario, + isScenarioApplicableAt, listActiveClientScenarios, listDraftClientScenarios, listPendingClientScenarios } from '../index'; -import { - DRAFT_PROTOCOL_VERSION, - LATEST_SPEC_VERSION, - type SpecVersion -} from '../../types'; +import { LATEST_SPEC_VERSION, type SpecVersion } from '../../types'; import path from 'path'; function getFreePort(): Promise { @@ -147,14 +145,11 @@ describe('Server Scenarios', () => { throw new Error(`Scenario ${scenarioName} not found`); } - // Draft-only scenarios expect the draft (stateless) connection. Other - // scenarios normally use the latest stateful wire unless a test overrides it. + // Mirror the runner: without an override each scenario runs at the + // latest release if it applies there, else the newest revision in its + // window (stateful-only scenarios land on 2025-11-25). const targetSpecVersion = - specVersion ?? - ('introducedIn' in scenario.source && - scenario.source.introducedIn === DRAFT_PROTOCOL_VERSION - ? DRAFT_PROTOCOL_VERSION - : LATEST_SPEC_VERSION); + specVersion ?? defaultSpecVersionFor(scenario.source); const checks = await scenario.run( testContext(serverUrl, targetSpecVersion) @@ -183,20 +178,19 @@ describe('Server Scenarios', () => { }, 10000); // 10 second timeout per scenario } - // These scenarios are introduced before the stateless protocol, so the normal - // fixture matrix exercises them on the latest stateful wire. Run them again on - // the modern wire to cover the streamed response adapter used by tools/call. - for (const scenarioName of [ - 'tools-call-simple-text', - 'tools-call-image', - 'tools-call-audio', - 'tools-call-embedded-resource', - 'tools-call-mixed-content', - 'tools-call-error', - 'tools-call-with-progress' - ]) { - it(`${scenarioName} on ${DRAFT_PROTOCOL_VERSION}`, async () => { - await expectScenarioToPass(scenarioName, DRAFT_PROTOCOL_VERSION); - }, 10000); + // Scenarios that span both wire eras run above at the latest (stateless) + // release; run them again on the last stateful revision so the initialize + // handshake path of each stays covered too. + const LAST_STATEFUL: SpecVersion = '2025-11-25'; + for (const scenarioName of scenarios) { + const scenario = getClientScenario(scenarioName)!; + if ( + defaultSpecVersionFor(scenario.source) === LATEST_SPEC_VERSION && + isScenarioApplicableAt(scenario.source, LAST_STATEFUL) + ) { + it(`${scenarioName} on ${LAST_STATEFUL}`, async () => { + await expectScenarioToPass(scenarioName, LAST_STATEFUL); + }, 10000); + } } }); diff --git a/src/scenarios/server/caching.ts b/src/scenarios/server/caching.ts index c7f4a5b5..a698e710 100644 --- a/src/scenarios/server/caching.ts +++ b/src/scenarios/server/caching.ts @@ -5,17 +5,13 @@ * tools/list, prompts/list, resources/list, resources/templates/list, resources/read */ -import { - ClientScenario, - ConformanceCheck, - DRAFT_PROTOCOL_VERSION -} from '../../types'; +import { ClientScenario, ConformanceCheck } from '../../types'; import { JsonRpcError, type Connection, type RunContext } from '../../connection'; -import type { CacheableResult } from '../../spec-types/draft'; +import type { CacheableResult } from '../../spec-types/2026-07-28'; const SPEC_REFS = [ { @@ -80,7 +76,7 @@ function buildPresenceCheck( export class CachingScenario implements ClientScenario { name = 'caching'; - readonly source = { introducedIn: DRAFT_PROTOCOL_VERSION } as const; + readonly source = { introducedIn: '2026-07-28' } as const; description = `Test that servers include caching hints (ttlMs and cacheScope) on cacheable results (SEP-2549). **Server Implementation Requirements:** @@ -96,10 +92,10 @@ Servers MUST include \`ttlMs\` (integer >= 0) and \`cacheScope\` ("public" or "p const checks: ConformanceCheck[] = []; const allFields: Array<{ endpoint: string; fields: CachingFields }> = []; - // SEP-2549 only exists in the draft spec, so each cacheable endpoint is - // queried over the version-appropriate connection. Under --spec-version - // draft that resolves to the stateless impl (SEP-2575): protocolVersion - // 2026-07-28 plus the cross-cutting _meta and standard headers + // SEP-2549 exists from 2026-07-28, so each cacheable endpoint is queried + // over the version-appropriate connection, which at that revision (and + // later) resolves to the stateless impl (SEP-2575): the run's + // protocolVersion plus the cross-cutting _meta and standard headers // (issue #315). let conn: Connection; try { diff --git a/src/scenarios/server/dns-rebinding.ts b/src/scenarios/server/dns-rebinding.ts index e93f810c..4627ef45 100644 --- a/src/scenarios/server/dns-rebinding.ts +++ b/src/scenarios/server/dns-rebinding.ts @@ -8,9 +8,13 @@ import { ClientScenario, ConformanceCheck, - DRAFT_PROTOCOL_VERSION + protocolVersionFor } from '../../types'; -import { buildStandardHeaders, type RunContext } from '../../connection'; +import { + buildStandardHeaders, + isStateless, + type RunContext +} from '../../connection'; import { request } from 'undici'; const SPEC_REFERENCES = [ @@ -51,7 +55,10 @@ function getHostFromUrl(serverUrl: string): string { * accept without prior setup (initialize for the stateful lifecycle, * server/discover with _meta for the stateless lifecycle). */ -function probeBody(specVersion: string): { +function probeBody( + stateless: boolean, + protocolVersion: string +): { jsonrpc: '2.0'; id: number; method: string; @@ -61,14 +68,14 @@ function probeBody(specVersion: string): { name: 'conformance-dns-rebinding-test', version: '1.0.0' }; - if (specVersion === DRAFT_PROTOCOL_VERSION) { + if (stateless) { return { jsonrpc: '2.0', id: 1, method: 'server/discover', params: { _meta: { - 'io.modelcontextprotocol/protocolVersion': specVersion, + 'io.modelcontextprotocol/protocolVersion': protocolVersion, 'io.modelcontextprotocol/clientInfo': clientInfo, 'io.modelcontextprotocol/clientCapabilities': {} } @@ -80,7 +87,7 @@ function probeBody(specVersion: string): { id: 1, method: 'initialize', params: { - protocolVersion: specVersion, + protocolVersion, capabilities: {}, clientInfo } @@ -95,20 +102,21 @@ function probeBody(specVersion: string): { async function sendRequestWithHostAndOrigin( serverUrl: string, hostOrOrigin: string, - specVersion: string + stateless: boolean, + protocolVersion: string ): Promise<{ statusCode: number; body: unknown }> { // Build the SEP-2243 standard headers (Mcp-Method, Accept, ...) for the // probe's JSON-RPC method so a strictly-conformant server only rejects the // request for the Host/Origin values under test, then layer the // scenario-specific headers on top. - const probe = probeBody(specVersion); + const probe = probeBody(stateless, protocolVersion); const response = await request(serverUrl, { method: 'POST', headers: buildStandardHeaders(probe.method, probe.params, { headers: { Host: hostOrOrigin, Origin: `http://${hostOrOrigin}`, - 'MCP-Protocol-Version': specVersion + 'MCP-Protocol-Version': protocolVersion } }), body: JSON.stringify(probe) @@ -154,7 +162,9 @@ website tricks a user's browser into making requests to the local server. See: https://github.com/modelcontextprotocol/typescript-sdk/security/advisories/GHSA-w48q-cv73-mx4w`; async run(ctx: RunContext): Promise { - const { serverUrl, specVersion } = ctx; + const { serverUrl } = ctx; + const stateless = isStateless(ctx); + const protocolVersion = protocolVersionFor(ctx.specVersion); const checks: ConformanceCheck[] = []; const timestamp = new Date().toISOString(); @@ -206,7 +216,8 @@ See: https://github.com/modelcontextprotocol/typescript-sdk/security/advisories/ const response = await sendRequestWithHostAndOrigin( serverUrl, attackerHost, - specVersion + stateless, + protocolVersion ); const isRejected = response.statusCode >= 400 && response.statusCode < 500; @@ -249,7 +260,8 @@ See: https://github.com/modelcontextprotocol/typescript-sdk/security/advisories/ const response = await sendRequestWithHostAndOrigin( serverUrl, validHost, - specVersion + stateless, + protocolVersion ); const isAccepted = response.statusCode >= 200 && response.statusCode < 300; diff --git a/src/scenarios/server/elicitation-defaults.ts b/src/scenarios/server/elicitation-defaults.ts index 20ed8ee9..b71291e6 100644 --- a/src/scenarios/server/elicitation-defaults.ts +++ b/src/scenarios/server/elicitation-defaults.ts @@ -2,11 +2,7 @@ * SEP-1034: Elicitation default values test scenarios for MCP servers */ -import { - ClientScenario, - ConformanceCheck, - DRAFT_PROTOCOL_VERSION -} from '../../types'; +import { ClientScenario, ConformanceCheck } from '../../types'; import type { RunContext } from '../../connection'; import { connectToServer } from '../../connection/sdk-client'; import { ElicitRequestSchema } from '@modelcontextprotocol/sdk/types.js'; @@ -15,7 +11,7 @@ export class ElicitationDefaultsScenario implements ClientScenario { name = 'elicitation-sep1034-defaults'; readonly source = { introducedIn: '2025-11-25', - removedIn: DRAFT_PROTOCOL_VERSION + removedIn: '2026-07-28' } as const; description = `Test elicitation with default values for all primitive types (SEP-1034). diff --git a/src/scenarios/server/elicitation-enums.ts b/src/scenarios/server/elicitation-enums.ts index ac048600..a6a70dd8 100644 --- a/src/scenarios/server/elicitation-enums.ts +++ b/src/scenarios/server/elicitation-enums.ts @@ -2,11 +2,7 @@ * SEP-1330: Elicitation enum schema improvements test scenarios for MCP servers */ -import { - ClientScenario, - ConformanceCheck, - DRAFT_PROTOCOL_VERSION -} from '../../types'; +import { ClientScenario, ConformanceCheck } from '../../types'; import type { RunContext } from '../../connection'; import { connectToServer } from '../../connection/sdk-client'; import { ElicitRequestSchema } from '@modelcontextprotocol/sdk/types.js'; @@ -15,7 +11,7 @@ export class ElicitationEnumsScenario implements ClientScenario { name = 'elicitation-sep1330-enums'; readonly source = { introducedIn: '2025-11-25', - removedIn: DRAFT_PROTOCOL_VERSION + removedIn: '2026-07-28' } as const; description = `Test elicitation with enum schema improvements (SEP-1330). diff --git a/src/scenarios/server/http-standard-headers.ts b/src/scenarios/server/http-standard-headers.ts index d5ca0320..6627c743 100644 --- a/src/scenarios/server/http-standard-headers.ts +++ b/src/scenarios/server/http-standard-headers.ts @@ -18,14 +18,16 @@ import http from 'http'; import { ClientScenario, ConformanceCheck, - DRAFT_PROTOCOL_VERSION + LATEST_SPEC_VERSION, + SpecVersion, + protocolVersionFor } from '../../types'; import { withRequestMeta, sendStatelessRequest, type RunContext } from '../../connection'; -import { HEADER_MISMATCH } from '../../spec-types/draft'; +import { HEADER_MISMATCH } from '../../spec-types/2026-07-28'; import { untestableCheck } from '../untestable'; const SPEC_REFERENCE = { @@ -291,7 +293,7 @@ function createAcceptanceCheck( export class HttpHeaderValidationScenario implements ClientScenario { name = 'http-header-validation'; - readonly source = { introducedIn: DRAFT_PROTOCOL_VERSION } as const; + readonly source = { introducedIn: '2026-07-28' } as const; description = `Test server validation of standard MCP request headers (SEP-2243). **Server Implementation Requirements:** @@ -307,14 +309,23 @@ export class HttpHeaderValidationScenario implements ClientScenario { - Server MUST return HTTP 400 Bad Request for validation failures - Server MUST return JSON-RPC error with code -32020 (HeaderMismatch)`; + /** Spec version of the current run; requests declare its wire version. */ + private specVersion: SpecVersion = LATEST_SPEC_VERSION; + async run(ctx: RunContext): Promise { const { serverUrl } = ctx; + this.specVersion = ctx.specVersion; const checks: ConformanceCheck[] = []; try { // Discover the server's tools with a fully-conformant stateless request // (SEP-2575) — that wire protocol has no initialize handshake or sessions. - const toolsResponse = await sendStatelessRequest(serverUrl, 'tools/list'); + const toolsResponse = await sendStatelessRequest( + serverUrl, + 'tools/list', + undefined, + { specVersion: ctx.specVersion } + ); if (!toolsResponse.body?.result) { // The server under test could not even answer a conformant tools/list: // report a single explicit setup failure instead of misleading @@ -345,7 +356,7 @@ export class HttpHeaderValidationScenario implements ClientScenario { }; const baseHeaders: Record = { - 'MCP-Protocol-Version': DRAFT_PROTOCOL_VERSION + 'MCP-Protocol-Version': protocolVersionFor(ctx.specVersion) }; let idCounter = 100; @@ -552,7 +563,7 @@ export class HttpHeaderValidationScenario implements ClientScenario { const requestBody = { ...body, id: body.id === 0 ? nextId() : body.id, - params: withRequestMeta(body.params) + params: withRequestMeta(body.params, this.specVersion) }; const response = await sendRawRequest(serverUrl, requestBody, { ...baseHeaders, @@ -602,7 +613,7 @@ export class HttpHeaderValidationScenario implements ClientScenario { export class HttpCustomHeaderServerValidationScenario implements ClientScenario { name = 'http-custom-header-server-validation'; - readonly source = { introducedIn: DRAFT_PROTOCOL_VERSION } as const; + readonly source = { introducedIn: '2026-07-28' } as const; description = `Test server validation of custom Mcp-Param headers and Base64 encoding (SEP-2243). **Server Implementation Requirements:** @@ -615,14 +626,23 @@ export class HttpCustomHeaderServerValidationScenario implements ClientScenario - Server MUST treat values without =?base64?...?= wrapper as literal - Server MUST reject requests where custom header is omitted but value is in body`; + /** Spec version of the current run; requests declare its wire version. */ + private specVersion: SpecVersion = LATEST_SPEC_VERSION; + async run(ctx: RunContext): Promise { const { serverUrl } = ctx; + this.specVersion = ctx.specVersion; const checks: ConformanceCheck[] = []; try { // Discover the server's tools with a fully-conformant stateless request // (SEP-2575) — that wire protocol has no initialize handshake or sessions. - const toolsResponse = await sendStatelessRequest(serverUrl, 'tools/list'); + const toolsResponse = await sendStatelessRequest( + serverUrl, + 'tools/list', + undefined, + { specVersion: ctx.specVersion } + ); if (!toolsResponse.body?.result) { // The server under test could not even answer a conformant tools/list: // report a single explicit setup failure (and backfill the declared @@ -677,7 +697,7 @@ export class HttpCustomHeaderServerValidationScenario implements ClientScenario } const baseHeaders: Record = { - 'MCP-Protocol-Version': DRAFT_PROTOCOL_VERSION + 'MCP-Protocol-Version': protocolVersionFor(ctx.specVersion) }; // Find the first x-mcp-header annotated STRING property @@ -944,10 +964,13 @@ export class HttpCustomHeaderServerValidationScenario implements ClientScenario method: 'tools/call', // Issue #311: the body always carries the SEP-2575 _meta fields — // these cases only vary the Mcp-Param header value. - params: withRequestMeta({ - name: toolName, - arguments: { ...defaultArgs, [paramName]: bodyValue } - }) + params: withRequestMeta( + { + name: toolName, + arguments: { ...defaultArgs, [paramName]: bodyValue } + }, + this.specVersion + ) }, { ...baseHeaders, @@ -1030,10 +1053,13 @@ export class HttpCustomHeaderServerValidationScenario implements ClientScenario method: 'tools/call', // Issue #311: the body always carries the SEP-2575 _meta fields — // this case only omits the Mcp-Param header. - params: withRequestMeta({ - name: toolName, - arguments: { ...defaultArgs, [paramName]: 'test-value' } - }) + params: withRequestMeta( + { + name: toolName, + arguments: { ...defaultArgs, [paramName]: 'test-value' } + }, + this.specVersion + ) }, { ...baseHeaders, diff --git a/src/scenarios/server/input-required-result.ts b/src/scenarios/server/input-required-result.ts index 64ad4ba9..fdaca1e5 100644 --- a/src/scenarios/server/input-required-result.ts +++ b/src/scenarios/server/input-required-result.ts @@ -6,12 +6,7 @@ * clients retry with inputResponses and echoed requestState. */ -import { - ClientScenario, - ConformanceCheck, - DRAFT_PROTOCOL_VERSION, - SpecVersion -} from '../../types'; +import { ClientScenario, ConformanceCheck } from '../../types'; import type { RunContext } from '../../connection'; import { sendRpc, @@ -28,8 +23,7 @@ import { notTestable } from '../untestable'; export class InputRequiredResultBasicElicitationScenario implements ClientScenario { name = 'input-required-result-basic-elicitation'; - readonly source = { introducedIn: DRAFT_PROTOCOL_VERSION } as const; - specVersions: SpecVersion[] = [DRAFT_PROTOCOL_VERSION]; + readonly source = { introducedIn: '2026-07-28' } as const; description = `Test basic ephemeral InputRequiredResult flow with a single elicitation input request (SEP-2322). **Server Implementation Requirements:** @@ -183,8 +177,7 @@ Implement a tool named \`test_input_required_result_elicitation\` (no arguments export class InputRequiredResultBasicSamplingScenario implements ClientScenario { name = 'input-required-result-basic-sampling'; - readonly source = { introducedIn: DRAFT_PROTOCOL_VERSION } as const; - specVersions: SpecVersion[] = [DRAFT_PROTOCOL_VERSION]; + readonly source = { introducedIn: '2026-07-28' } as const; description = `Test basic ephemeral InputRequiredResult flow with a single sampling input request (SEP-2322). **Server Implementation Requirements:** @@ -325,8 +318,7 @@ Implement a tool named \`test_input_required_result_sampling\` (no arguments req export class InputRequiredResultBasicListRootsScenario implements ClientScenario { name = 'input-required-result-basic-list-roots'; - readonly source = { introducedIn: DRAFT_PROTOCOL_VERSION } as const; - specVersions: SpecVersion[] = [DRAFT_PROTOCOL_VERSION]; + readonly source = { introducedIn: '2026-07-28' } as const; description = `Test basic ephemeral InputRequiredResult flow with a single roots/list input request (SEP-2322). **Server Implementation Requirements:** @@ -461,8 +453,7 @@ Implement a tool named \`test_input_required_result_list_roots\` (no arguments r export class InputRequiredResultRequestStateScenario implements ClientScenario { name = 'input-required-result-request-state'; - readonly source = { introducedIn: DRAFT_PROTOCOL_VERSION } as const; - specVersions: SpecVersion[] = [DRAFT_PROTOCOL_VERSION]; + readonly source = { introducedIn: '2026-07-28' } as const; description = `Test that requestState is correctly round-tripped in ephemeral InputRequiredResult flow (SEP-2322). **Server Implementation Requirements:** @@ -593,8 +584,7 @@ Implement a tool named \`test_input_required_result_request_state\` (no argument export class InputRequiredResultMultipleInputRequestsScenario implements ClientScenario { name = 'input-required-result-multiple-input-requests'; - readonly source = { introducedIn: DRAFT_PROTOCOL_VERSION } as const; - specVersions: SpecVersion[] = [DRAFT_PROTOCOL_VERSION]; + readonly source = { introducedIn: '2026-07-28' } as const; description = `Test multiple input requests in a single InputRequiredResult (SEP-2322). **Server Implementation Requirements:** @@ -768,8 +758,7 @@ Implement a tool named \`test_input_required_result_multiple_inputs\` (no argume export class InputRequiredResultMultiRoundScenario implements ClientScenario { name = 'input-required-result-multi-round'; - readonly source = { introducedIn: DRAFT_PROTOCOL_VERSION } as const; - specVersions: SpecVersion[] = [DRAFT_PROTOCOL_VERSION]; + readonly source = { introducedIn: '2026-07-28' } as const; description = `Test multi-round ephemeral InputRequiredResult flow with evolving requestState (SEP-2322). **Server Implementation Requirements:** @@ -953,8 +942,7 @@ Implement a tool named \`test_input_required_result_multi_round\` (no arguments export class InputRequiredResultMissingInputResponseScenario implements ClientScenario { name = 'input-required-result-missing-input-response'; - readonly source = { introducedIn: DRAFT_PROTOCOL_VERSION } as const; - specVersions: SpecVersion[] = [DRAFT_PROTOCOL_VERSION]; + readonly source = { introducedIn: '2026-07-28' } as const; description = `Test error handling when client sends wrong/missing inputResponses (SEP-2322). **Server Implementation Requirements:** @@ -1027,8 +1015,7 @@ Use the same tool as A1: \`test_input_required_result_elicitation\`. export class InputRequiredResultNonToolRequestScenario implements ClientScenario { name = 'input-required-result-non-tool-request'; - readonly source = { introducedIn: DRAFT_PROTOCOL_VERSION } as const; - specVersions: SpecVersion[] = [DRAFT_PROTOCOL_VERSION]; + readonly source = { introducedIn: '2026-07-28' } as const; description = `Test InputRequiredResult on a non-tool request (prompts/get) to verify InputRequiredResult is universal (SEP-2322). **Server Implementation Requirements:** @@ -1152,8 +1139,7 @@ Implement a prompt named \`test_input_required_result_prompt\` that requires eli export class InputRequiredResultResultTypeScenario implements ClientScenario { name = 'input-required-result-result-type'; - readonly source = { introducedIn: DRAFT_PROTOCOL_VERSION } as const; - specVersions: SpecVersion[] = [DRAFT_PROTOCOL_VERSION]; + readonly source = { introducedIn: '2026-07-28' } as const; description = `Test that server explicitly includes resultType field in InputRequiredResult responses (SEP-2322). **Server Implementation Requirements:** @@ -1221,8 +1207,7 @@ This scenario verifies that the resultType field is explicitly present in the re export class InputRequiredResultUnsupportedMethodsScenario implements ClientScenario { name = 'input-required-result-unsupported-methods'; - readonly source = { introducedIn: DRAFT_PROTOCOL_VERSION } as const; - specVersions: SpecVersion[] = [DRAFT_PROTOCOL_VERSION]; + readonly source = { introducedIn: '2026-07-28' } as const; description = `Test that server does NOT return InputRequiredResult on unsupported methods (SEP-2322). Servers MUST NOT send InputRequiredResult responses on any client requests other than the supported ones (prompts/get, resources/read, tools/call, tasks/result).`; @@ -1279,8 +1264,7 @@ Servers MUST NOT send InputRequiredResult responses on any client requests other export class InputRequiredResultTamperedStateScenario implements ClientScenario { name = 'input-required-result-tampered-state'; - readonly source = { introducedIn: DRAFT_PROTOCOL_VERSION } as const; - specVersions: SpecVersion[] = [DRAFT_PROTOCOL_VERSION]; + readonly source = { introducedIn: '2026-07-28' } as const; description = `Test that server rejects tampered requestState (SEP-2322). **Server Implementation Requirements:** @@ -1394,8 +1378,7 @@ JSON-RPC error (code -32602 or similar) indicating integrity check failure.`; export class InputRequiredResultCapabilityCheckScenario implements ClientScenario { name = 'input-required-result-capability-check'; - readonly source = { introducedIn: DRAFT_PROTOCOL_VERSION } as const; - specVersions: SpecVersion[] = [DRAFT_PROTOCOL_VERSION]; + readonly source = { introducedIn: '2026-07-28' } as const; description = `Test that server only sends inputRequests for capabilities the client declared (SEP-2322). **Server Implementation Requirements:** @@ -1492,8 +1475,7 @@ Only include inputRequests for methods the client supports. For example, if the export class InputRequiredResultIgnoreExtraParamsScenario implements ClientScenario { name = 'input-required-result-ignore-extra-params'; - readonly source = { introducedIn: DRAFT_PROTOCOL_VERSION } as const; - specVersions: SpecVersion[] = [DRAFT_PROTOCOL_VERSION]; + readonly source = { introducedIn: '2026-07-28' } as const; description = `Test that server ignores unexpected extra parameters in InputResponses (SEP-2322). **Server Implementation Requirements:** @@ -1566,8 +1548,7 @@ the extra keys and complete normally.`; export class InputRequiredResultValidateInputScenario implements ClientScenario { name = 'input-required-result-validate-input'; - readonly source = { introducedIn: DRAFT_PROTOCOL_VERSION } as const; - specVersions: SpecVersion[] = [DRAFT_PROTOCOL_VERSION]; + readonly source = { introducedIn: '2026-07-28' } as const; description = `Test that server validates InputResponses and returns appropriate errors (SEP-2322). **Server Implementation Requirements:** diff --git a/src/scenarios/server/json-schema-2020-12.ts b/src/scenarios/server/json-schema-2020-12.ts index d1487a5e..3d885152 100644 --- a/src/scenarios/server/json-schema-2020-12.ts +++ b/src/scenarios/server/json-schema-2020-12.ts @@ -15,7 +15,8 @@ import { CheckStatus, ClientScenario, ConformanceCheck, - DRAFT_PROTOCOL_VERSION + SpecVersion, + specVersionAtLeast } from '../../types.js'; import type { RunContext } from '../../connection'; import type { ListToolsResult } from '../../spec-types/2025-11-25'; @@ -69,18 +70,20 @@ export const JSON_SCHEMA_2020_12_FIXTURE = { * Preserving the broader JSON Schema 2020-12 vocabulary is good behavior at * any protocol version, so a server that preserves the keywords gets SUCCESS * regardless. Stripping them is only a conformance FAILURE when the run - * targets a protocol version that includes SEP-2106 (the draft); when - * targeting an earlier dated version the requirement does not apply, so the - * check is SKIPPED rather than failed. + * targets a protocol version that includes SEP-2106 ({@link SEP_2106_SINCE} + * onward); when targeting an earlier dated version the requirement does not + * apply, so the check is SKIPPED rather than failed. */ +export const SEP_2106_SINCE: SpecVersion = '2026-07-28'; + export function sep2106KeywordCheckStatus( preserved: boolean, - targetProtocolVersion: string + targetVersion: SpecVersion ): CheckStatus { if (preserved) { return 'SUCCESS'; } - return targetProtocolVersion === DRAFT_PROTOCOL_VERSION + return specVersionAtLeast(targetVersion, SEP_2106_SINCE) ? 'FAILURE' : 'SKIPPED'; } @@ -237,9 +240,9 @@ ${JSON.stringify( // inputSchema and must survive tools/list rather than being stripped to // properties/required. These checks are soft-gated on the protocol // version this run targets (see sep2106KeywordCheckStatus): stripping - // the keywords is only a FAILURE when targeting the draft version, and - // SKIPPED otherwise. - const skippedSuffix = ` (run targets protocol version ${targetVersion}; SEP-2106 applies from ${DRAFT_PROTOCOL_VERSION})`; + // the keywords is only a FAILURE when targeting a version that includes + // SEP-2106, and SKIPPED otherwise. + const skippedSuffix = ` (run targets protocol version ${targetVersion}; SEP-2106 applies from ${SEP_2106_SINCE})`; // Check 5: composition keywords (allOf / anyOf) preserved const allOf = inputSchema['allOf']; diff --git a/src/scenarios/server/lifecycle.ts b/src/scenarios/server/lifecycle.ts index 85101b22..0c6cecf7 100644 --- a/src/scenarios/server/lifecycle.ts +++ b/src/scenarios/server/lifecycle.ts @@ -2,11 +2,7 @@ * Lifecycle test scenarios for MCP servers */ -import { - ClientScenario, - ConformanceCheck, - DRAFT_PROTOCOL_VERSION -} from '../../types'; +import { ClientScenario, ConformanceCheck } from '../../types'; import type { RunContext } from '../../connection'; import { connectToServer, @@ -31,7 +27,7 @@ export class ServerInitializeScenario implements ClientScenario { name = 'server-initialize'; readonly source = { introducedIn: '2025-06-18', - removedIn: DRAFT_PROTOCOL_VERSION + removedIn: '2026-07-28' } as const; description = `Test basic server initialization handshake. diff --git a/src/scenarios/server/negative.test.ts b/src/scenarios/server/negative.test.ts index 0d248272..c1940320 100644 --- a/src/scenarios/server/negative.test.ts +++ b/src/scenarios/server/negative.test.ts @@ -8,7 +8,12 @@ import { JsonSchema2020_12Scenario, sep2106KeywordCheckStatus } from './json-schema-2020-12'; -import { DRAFT_PROTOCOL_VERSION, LATEST_SPEC_VERSION } from '../../types'; +import { DRAFT_SPEC_VERSION } from '../../types'; + +// These fixtures speak one wire each: the stateless ones 2026-07-28, the +// SDK-based ones the last stateful revision. +const STATELESS = '2026-07-28' as const; +const STATEFUL = '2025-11-25' as const; import { takeWireViolations } from '../../validation/wire-schema'; function startServer(scriptPath: string, port: number): Promise { @@ -77,7 +82,7 @@ describe('Server scenario negative tests', () => { it('emits FAILURE against a server without rebinding protection', async () => { const scenario = new DNSRebindingProtectionScenario(); const checks = await scenario.run( - testContext(`http://localhost:${PORT}/mcp`) + testContext(`http://localhost:${PORT}/mcp`, STATEFUL) ); const rebindingCheck = checks.find( @@ -108,7 +113,7 @@ describe('Server scenario negative tests', () => { it('emits FAILURE for no-empty-contents and WARNING for error-code against a server returning empty contents', async () => { const scenario = new ResourcesNotFoundErrorScenario(); const checks = await scenario.run( - testContext(`http://localhost:${PORT}/mcp`, DRAFT_PROTOCOL_VERSION) + testContext(`http://localhost:${PORT}/mcp`, STATELESS) ); const noEmpty = checks.find((c) => c.id === 'sep-2164-no-empty-contents'); @@ -140,7 +145,7 @@ describe('Server scenario negative tests', () => { it('emits FAILURE for presence checks against a server without caching hints', async () => { const scenario = new CachingScenario(); const checks = await scenario.run( - testContext(`http://localhost:${PORT}/mcp`, DRAFT_PROTOCOL_VERSION) + testContext(`http://localhost:${PORT}/mcp`, STATELESS) ); // Should have at least 7 checks (5 presence + 2 aggregate) @@ -187,7 +192,7 @@ describe('Server scenario negative tests', () => { it('flags SEP-2106 keyword-preservation checks against a server that strips the 2020-12 vocabulary', async () => { const scenario = new JsonSchema2020_12Scenario(); const checks = await scenario.run( - testContext(`http://localhost:${PORT}/mcp`) + testContext(`http://localhost:${PORT}/mcp`, STATEFUL) ); // The tool is still advertised, so it must be found... @@ -196,8 +201,8 @@ describe('Server scenario negative tests', () => { ); expect(found?.status).toBe('SUCCESS'); - // ...but the stripped 2020-12 keywords must be flagged. testContext() - // defaults to LATEST_SPEC_VERSION (2025-11-25), so the soft version gate + // ...but the stripped 2020-12 keywords must be flagged. This fixture is + // driven at 2025-11-25 (before SEP-2106), so the soft version gate // reports SKIPPED rather than FAILURE; see sep2106KeywordCheckStatus. const composition = checks.find( (c) => c.id === 'sep-2106-composition-keywords-preserved' @@ -218,21 +223,16 @@ describe('Server scenario negative tests', () => { describe('sep2106KeywordCheckStatus (soft version gate)', () => { it('passes preserved keywords at any target version', () => { - expect(sep2106KeywordCheckStatus(true, DRAFT_PROTOCOL_VERSION)).toBe( - 'SUCCESS' - ); - expect(sep2106KeywordCheckStatus(true, LATEST_SPEC_VERSION)).toBe( - 'SUCCESS' - ); + expect(sep2106KeywordCheckStatus(true, STATELESS)).toBe('SUCCESS'); + expect(sep2106KeywordCheckStatus(true, STATEFUL)).toBe('SUCCESS'); }); - it('fails stripped keywords only when targeting the draft version', () => { - expect(sep2106KeywordCheckStatus(false, DRAFT_PROTOCOL_VERSION)).toBe( + it('fails stripped keywords only from 2026-07-28 onward', () => { + expect(sep2106KeywordCheckStatus(false, STATELESS)).toBe('FAILURE'); + expect(sep2106KeywordCheckStatus(false, DRAFT_SPEC_VERSION)).toBe( 'FAILURE' ); - expect(sep2106KeywordCheckStatus(false, LATEST_SPEC_VERSION)).toBe( - 'SKIPPED' - ); + expect(sep2106KeywordCheckStatus(false, STATEFUL)).toBe('SKIPPED'); }); }); }); diff --git a/src/scenarios/server/resources.ts b/src/scenarios/server/resources.ts index 382b0db8..6fd85602 100644 --- a/src/scenarios/server/resources.ts +++ b/src/scenarios/server/resources.ts @@ -2,11 +2,7 @@ * Resources test scenarios for MCP servers */ -import { - ClientScenario, - ConformanceCheck, - DRAFT_PROTOCOL_VERSION -} from '../../types'; +import { ClientScenario, ConformanceCheck } from '../../types'; import { JsonRpcError, type RunContext } from '../../connection'; import type { ListResourcesResult, @@ -375,7 +371,7 @@ export class ResourcesSubscribeScenario implements ClientScenario { name = 'resources-subscribe'; readonly source = { introducedIn: '2025-06-18', - removedIn: DRAFT_PROTOCOL_VERSION + removedIn: '2026-07-28' } as const; description = `Test subscribing to resource updates. @@ -447,7 +443,7 @@ Example request: export class ResourcesNotFoundErrorScenario implements ClientScenario { name = 'sep-2164-resource-not-found'; - readonly source = { introducedIn: DRAFT_PROTOCOL_VERSION } as const; + readonly source = { introducedIn: '2026-07-28' } as const; description = `Test error handling for non-existent resources (SEP-2164). **Server Implementation Requirements:** @@ -489,9 +485,9 @@ This scenario does not require the server to register any specific resource — } ]; - // SEP-2164 is a draft-spec requirement; under --spec-version draft the + // SEP-2164 is a 2026-07-28 requirement; at that revision (and later) the // RunContext-provided connect() resolves to the stateless impl, so the - // request goes out with the draft protocol version and _meta/headers. + // request goes out with the run's protocol version and _meta/headers. let conn; try { conn = await ctx.connect(); @@ -605,7 +601,7 @@ export class ResourcesUnsubscribeScenario implements ClientScenario { name = 'resources-unsubscribe'; readonly source = { introducedIn: '2025-06-18', - removedIn: DRAFT_PROTOCOL_VERSION + removedIn: '2026-07-28' } as const; description = `Test unsubscribing from resource. diff --git a/src/scenarios/server/session-lifecycle.ts b/src/scenarios/server/session-lifecycle.ts index b5facbd4..d67945b9 100644 --- a/src/scenarios/server/session-lifecycle.ts +++ b/src/scenarios/server/session-lifecycle.ts @@ -11,11 +11,7 @@ * See https://modelcontextprotocol.io/specification/2025-11-25/basic/transports#session-management */ -import { - ClientScenario, - ConformanceCheck, - DRAFT_PROTOCOL_VERSION -} from '../../types'; +import { ClientScenario, ConformanceCheck } from '../../types'; import { readSseJsonRpcResponse, type RunContext } from '../../connection'; const SPEC_REFERENCES = [ @@ -134,11 +130,11 @@ async function negotiatedProtocolVersion( export class SessionLifecycleScenario implements ClientScenario { name = 'server-session-lifecycle'; - // Sessions were removed entirely from the draft spec (SEP-2575), so the - // lifecycle checks only apply to the dated stateful versions. + // Sessions were removed entirely in 2026-07-28 (SEP-2575), so the + // lifecycle checks only apply to the earlier stateful versions. readonly source = { introducedIn: '2025-03-26', - removedIn: DRAFT_PROTOCOL_VERSION + removedIn: '2026-07-28' } as const; description = `Verify the server honours the streamable-HTTP session termination contract. diff --git a/src/scenarios/server/sse-multiple-streams.ts b/src/scenarios/server/sse-multiple-streams.ts index 91bd24ca..86a6f7d5 100644 --- a/src/scenarios/server/sse-multiple-streams.ts +++ b/src/scenarios/server/sse-multiple-streams.ts @@ -12,9 +12,13 @@ import { ClientScenario, ConformanceCheck, - DRAFT_PROTOCOL_VERSION + protocolVersionFor } from '../../types.js'; -import { buildStandardHeaders, type RunContext } from '../../connection'; +import { + buildStandardHeaders, + isStateless, + type RunContext +} from '../../connection'; import { EventSourceParserStream } from 'eventsource-parser/stream'; import { Client } from '@modelcontextprotocol/sdk/client/index.js'; import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'; @@ -35,7 +39,7 @@ export class ServerSSEMultipleStreamsScenario implements ClientScenario { // scaffolding differs: the stateful lifecycle needs a session id from an // initialize handshake, the stateless lifecycle carries _meta + the // MCP-Protocol-Version header on each request instead. - const stateless = specVersion === DRAFT_PROTOCOL_VERSION; + const stateless = isStateless(ctx); let sessionId: string | undefined; let negotiatedProtocolVersion: string | undefined; @@ -99,12 +103,14 @@ export class ServerSSEMultipleStreamsScenario implements ClientScenario { 'Content-Type': 'application/json', Accept: 'text/event-stream, application/json', 'mcp-session-id': sessionId!, - 'mcp-protocol-version': negotiatedProtocolVersion ?? specVersion + 'mcp-protocol-version': + negotiatedProtocolVersion ?? protocolVersionFor(specVersion) }; const requestParams = stateless ? { _meta: { - 'io.modelcontextprotocol/protocolVersion': DRAFT_PROTOCOL_VERSION, + 'io.modelcontextprotocol/protocolVersion': + protocolVersionFor(specVersion), 'io.modelcontextprotocol/clientInfo': { name: 'conformance-test-client', version: '1.0.0' diff --git a/src/scenarios/server/sse-polling.ts b/src/scenarios/server/sse-polling.ts index 280fe791..c5b89bef 100644 --- a/src/scenarios/server/sse-polling.ts +++ b/src/scenarios/server/sse-polling.ts @@ -8,11 +8,7 @@ * - Replaying events when client reconnects with Last-Event-ID */ -import { - ClientScenario, - ConformanceCheck, - DRAFT_PROTOCOL_VERSION -} from '../../types.js'; +import { ClientScenario, ConformanceCheck } from '../../types.js'; import type { RunContext } from '../../connection'; import { EventSourceParserStream } from 'eventsource-parser/stream'; import { Client } from '@modelcontextprotocol/sdk/client/index.js'; @@ -75,7 +71,7 @@ export class ServerSSEPollingScenario implements ClientScenario { name = 'server-sse-polling'; readonly source = { introducedIn: '2025-11-25', - removedIn: DRAFT_PROTOCOL_VERSION + removedIn: '2026-07-28' } as const; description = 'Test server SSE polling via test_reconnection tool that closes stream mid-call (SEP-1699)'; diff --git a/src/scenarios/server/stateless.ts b/src/scenarios/server/stateless.ts index 618dc0c5..395d76ef 100644 --- a/src/scenarios/server/stateless.ts +++ b/src/scenarios/server/stateless.ts @@ -5,7 +5,7 @@ import { ClientScenario, ConformanceCheck, - DRAFT_PROTOCOL_VERSION + protocolVersionFor } from '../../types'; import { buildStandardHeaders, @@ -23,7 +23,7 @@ const SPEC_REF = [ export class ServerStatelessScenario implements ClientScenario { name = 'server-stateless'; - readonly source = { introducedIn: DRAFT_PROTOCOL_VERSION } as const; + readonly source = { introducedIn: '2026-07-28' } as const; description = `Test stateless MCP server architecture (SEP-2575). **Server Implementation Requirements:** @@ -56,6 +56,9 @@ export class ServerStatelessScenario implements ClientScenario { async run(ctx: RunContext): Promise { const { serverUrl, specVersion } = ctx; + // Wire string for hand-built _meta/header values (the draft maps to its + // current protocolVersion; dated revisions are their own). + const protocolVersion = protocolVersionFor(specVersion); const checks: ConformanceCheck[] = []; const timestamp = new Date().toISOString(); @@ -283,7 +286,7 @@ export class ServerStatelessScenario implements ClientScenario { }; const validMeta = { - 'io.modelcontextprotocol/protocolVersion': specVersion, + 'io.modelcontextprotocol/protocolVersion': protocolVersion, 'io.modelcontextprotocol/clientInfo': { name: 'conformance-client', version: '1.0.0' @@ -676,7 +679,7 @@ export class ServerStatelessScenario implements ClientScenario { const responseAbsent = await sendRpc( 'server/discover', { _meta: headerMismatchMeta }, - { 'MCP-Protocol-Version': specVersion }, + { 'MCP-Protocol-Version': protocolVersion }, 302 ).catch(() => null); const resAbsent: any = responseAbsent?.res ?? null; diff --git a/src/scenarios/server/tasks/capability.ts b/src/scenarios/server/tasks/capability.ts index 2a0bcda0..c08b68c0 100644 --- a/src/scenarios/server/tasks/capability.ts +++ b/src/scenarios/server/tasks/capability.ts @@ -12,7 +12,7 @@ import { ClientScenario, ConformanceCheck } from '../../../types'; import type { Connection, RunContext } from '../../../connection'; -import { MISSING_REQUIRED_CLIENT_CAPABILITY } from '../../../spec-types/draft'; +import { MISSING_REQUIRED_CLIENT_CAPABILITY } from '../../../spec-types/2026-07-28'; import { SEP_2575_REF, SEP_2663_REF } from './mrtr-helpers'; import { errMsg, failureCheck } from './mrtr-helpers'; import { TASKS_EXTENSION_ID } from './helpers'; diff --git a/src/scenarios/server/tasks/headers.ts b/src/scenarios/server/tasks/headers.ts index d523994d..253098c6 100644 --- a/src/scenarios/server/tasks/headers.ts +++ b/src/scenarios/server/tasks/headers.ts @@ -24,7 +24,7 @@ import { McpError } from '@modelcontextprotocol/sdk/types.js'; import { ClientScenario, ConformanceCheck } from '../../../types'; import type { Connection, RunContext } from '../../../connection'; -import { HEADER_MISMATCH } from '../../../spec-types/draft'; +import { HEADER_MISMATCH } from '../../../spec-types/2026-07-28'; import { SEP_2243_REF, SEP_2663_REF } from './mrtr-helpers'; import { errMsg, failureCheck } from './mrtr-helpers'; import { TASKS_EXTENSION_ID } from './helpers'; diff --git a/src/scenarios/server/tasks/lifecycle.test.ts b/src/scenarios/server/tasks/lifecycle.test.ts index 8926538b..97b0ce5d 100644 --- a/src/scenarios/server/tasks/lifecycle.test.ts +++ b/src/scenarios/server/tasks/lifecycle.test.ts @@ -1,7 +1,8 @@ import { describe, test, expect, afterEach } from 'vitest'; import { testContext } from '../../../connection/testing'; import { TasksLifecycleScenario } from './lifecycle'; -import { DRAFT_PROTOCOL_VERSION } from '../../../types'; +// Stateless-lifecycle tests pin the first stateless release. +const STATELESS = '2026-07-28' as const; import type { ConformanceCheck } from '../../../types'; /** @@ -21,7 +22,7 @@ function mockServer() { let result: any; if (body.method === 'server/discover') { result = { - supportedVersions: [DRAFT_PROTOCOL_VERSION], + supportedVersions: [STATELESS], capabilities: { tools: {} }, serverInfo: { name: 'taskless-server', version: '1.0.0' } }; @@ -66,7 +67,7 @@ describe('tasks-lifecycle — no task created', () => { const mockUrl = mockServer(); const scenario = new TasksLifecycleScenario(); const checks: ConformanceCheck[] = await scenario.run( - testContext(mockUrl, DRAFT_PROTOCOL_VERSION) + testContext(mockUrl, STATELESS) ); const gated = checks.filter((c) => diff --git a/src/scenarios/server/tasks/required-task-error.ts b/src/scenarios/server/tasks/required-task-error.ts index 1da07c8f..f6abe1c5 100644 --- a/src/scenarios/server/tasks/required-task-error.ts +++ b/src/scenarios/server/tasks/required-task-error.ts @@ -32,7 +32,7 @@ import { McpError } from '@modelcontextprotocol/sdk/types.js'; import { ClientScenario, ConformanceCheck } from '../../../types'; import type { Connection, RunContext } from '../../../connection'; -import { MISSING_REQUIRED_CLIENT_CAPABILITY } from '../../../spec-types/draft'; +import { MISSING_REQUIRED_CLIENT_CAPABILITY } from '../../../spec-types/2026-07-28'; import { SEP_2575_REF, SEP_2663_REF } from './mrtr-helpers'; import { errMsg } from './mrtr-helpers'; import { TASKS_EXTENSION_ID } from './helpers'; diff --git a/src/scenarios/server/tools.ts b/src/scenarios/server/tools.ts index f18df3cf..915d6041 100644 --- a/src/scenarios/server/tools.ts +++ b/src/scenarios/server/tools.ts @@ -2,11 +2,7 @@ * Tools test scenarios for MCP servers */ -import { - ClientScenario, - ConformanceCheck, - DRAFT_PROTOCOL_VERSION -} from '../../types'; +import { ClientScenario, ConformanceCheck } from '../../types'; import type { RunContext } from '../../connection'; import type { ListToolsResult, @@ -463,7 +459,7 @@ export class ToolsCallWithLoggingScenario implements ClientScenario { name = 'tools-call-with-logging'; readonly source = { introducedIn: '2025-06-18', - removedIn: DRAFT_PROTOCOL_VERSION + removedIn: '2026-07-28' } as const; description = `Test tool that sends log messages during execution. @@ -762,7 +758,7 @@ export class ToolsCallSamplingScenario implements ClientScenario { name = 'tools-call-sampling'; readonly source = { introducedIn: '2025-06-18', - removedIn: DRAFT_PROTOCOL_VERSION + removedIn: '2026-07-28' } as const; description = `Test tool that requests LLM sampling from client. @@ -894,7 +890,7 @@ export class ToolsCallElicitationScenario implements ClientScenario { name = 'tools-call-elicitation'; readonly source = { introducedIn: '2025-06-18', - removedIn: DRAFT_PROTOCOL_VERSION + removedIn: '2026-07-28' } as const; description = `Test tool that requests user input (elicitation) from client. diff --git a/src/scenarios/server/utils.ts b/src/scenarios/server/utils.ts index b2297158..fc7c20e6 100644 --- a/src/scenarios/server/utils.ts +++ b/src/scenarios/server/utils.ts @@ -2,11 +2,7 @@ * Utilities test scenarios for MCP servers */ -import { - ClientScenario, - ConformanceCheck, - DRAFT_PROTOCOL_VERSION -} from '../../types'; +import { ClientScenario, ConformanceCheck } from '../../types'; import type { RunContext } from '../../connection'; import type { CompleteResult, EmptyResult } from '../../spec-types/2025-06-18'; @@ -14,7 +10,7 @@ export class LoggingSetLevelScenario implements ClientScenario { name = 'logging-set-level'; readonly source = { introducedIn: '2025-06-18', - removedIn: DRAFT_PROTOCOL_VERSION + removedIn: '2026-07-28' } as const; description = `Test setting logging level. @@ -98,7 +94,7 @@ export class PingScenario implements ClientScenario { name = 'ping'; readonly source = { introducedIn: '2025-06-18', - removedIn: DRAFT_PROTOCOL_VERSION + removedIn: '2026-07-28' } as const; description = `Test ping utility for connection health check. diff --git a/src/scenarios/spec-version.test.ts b/src/scenarios/spec-version.test.ts index 7e5f4517..7dd99e38 100644 --- a/src/scenarios/spec-version.test.ts +++ b/src/scenarios/spec-version.test.ts @@ -10,15 +10,17 @@ import { getScenarioSpecVersions, matchesSpecVersion, resolveSpecVersion, + defaultSpecVersionFor, ALL_SPEC_VERSIONS, scenarios, clientScenarios } from './index'; import { DATED_SPEC_VERSIONS, - DRAFT_PROTOCOL_VERSION, + DRAFT_SPEC_VERSION, LATEST_SPEC_VERSION } from '../types'; +import { listRequirementRevisions } from '../requirements'; describe('specVersions helpers', () => { // The ScenarioSource union (introducedIn XOR extensionId) is enforced by the @@ -59,16 +61,30 @@ describe('specVersions helpers', () => { expect(current.length).toBeGreaterThan(overlap.length); }); - it('every scenario in latest but not in draft is explicitly removedIn: DRAFT', () => { + it('2026-07-28 drops the stateful-lifecycle scenarios and adds its own', () => { + const previous = new Set(listScenariosForSpec('2025-11-25')); + const current = new Set(listScenariosForSpec('2026-07-28')); + // removedIn: '2026-07-28' + expect(previous.has('initialize')).toBe(true); + expect(current.has('initialize')).toBe(false); + // introducedIn: '2026-07-28' + expect(previous.has('request-metadata')).toBe(false); + expect(current.has('request-metadata')).toBe(true); + expect(current.has('auth/iss-supported')).toBe(true); + // carried forward + expect(current.has('tools_call')).toBe(true); + }); + + it('every scenario in latest but not in draft is explicitly removedIn: draft', () => { const latest = new Set(listScenariosForSpec(LATEST_SPEC_VERSION)); - const draft = new Set(listScenariosForSpec(DRAFT_PROTOCOL_VERSION)); + const draft = new Set(listScenariosForSpec(DRAFT_SPEC_VERSION)); for (const name of latest) { if (!draft.has(name)) { const s = getScenario(name)!; expect( 'removedIn' in s.source && s.source.removedIn, `"${name}" is in ${LATEST_SPEC_VERSION} but not in draft without removedIn` - ).toBe(DRAFT_PROTOCOL_VERSION); + ).toBe(DRAFT_SPEC_VERSION); } } for (const name of listDraftScenarios()) { @@ -76,6 +92,13 @@ describe('specVersions helpers', () => { } }); + it('LATEST_SPEC_VERSION has a frozen requirement set', () => { + // tier-check defaults to the latest requirement set; promoting a new + // dated revision without freezing its requirements would leave that + // default pointing at the previous release. + expect(listRequirementRevisions()).toContain(LATEST_SPEC_VERSION); + }); + it('draft-introduced scenarios are not matched by any dated spec version', () => { for (const name of listDraftScenarios()) { for (const dated of DATED_SPEC_VERSIONS) { @@ -88,14 +111,50 @@ describe('specVersions helpers', () => { } }); - it("resolveSpecVersion accepts 'draft' as an alias", () => { - expect(resolveSpecVersion('draft')).toBe(DRAFT_PROTOCOL_VERSION); + it("resolveSpecVersion accepts 'draft' and dated versions", () => { + expect(resolveSpecVersion('draft')).toBe(DRAFT_SPEC_VERSION); + expect(resolveSpecVersion('2026-07-28')).toBe('2026-07-28'); expect(resolveSpecVersion(LATEST_SPEC_VERSION)).toBe(LATEST_SPEC_VERSION); }); + describe('defaultSpecVersionFor', () => { + it('uses the latest release for scenarios that apply there', () => { + expect(defaultSpecVersionFor({ introducedIn: '2025-06-18' })).toBe( + LATEST_SPEC_VERSION + ); + expect(defaultSpecVersionFor({ introducedIn: LATEST_SPEC_VERSION })).toBe( + LATEST_SPEC_VERSION + ); + }); + it('uses the draft for draft-only scenarios', () => { + expect(defaultSpecVersionFor({ introducedIn: DRAFT_SPEC_VERSION })).toBe( + DRAFT_SPEC_VERSION + ); + }); + it('uses the last release before removedIn for removed scenarios', () => { + expect( + defaultSpecVersionFor({ + introducedIn: '2025-06-18', + removedIn: '2026-07-28' + }) + ).toBe('2025-11-25'); + expect( + defaultSpecVersionFor({ + introducedIn: '2025-03-26', + removedIn: '2025-06-18' + }) + ).toBe('2025-03-26'); + }); + it('uses the latest release for extensions', () => { + expect( + defaultSpecVersionFor({ extensionId: 'io.modelcontextprotocol/tasks' }) + ).toBe(LATEST_SPEC_VERSION); + }); + }); + describe('matchesSpecVersion (per-check gating)', () => { const src = { introducedIn: '2025-11-25' } as const; - it.each(['2025-11-25', DRAFT_PROTOCOL_VERSION] as const)( + it.each(['2025-11-25', '2026-07-28', DRAFT_SPEC_VERSION] as const)( 'includes %s', (v) => expect(matchesSpecVersion(src, v)).toBe(true) ); @@ -123,7 +182,7 @@ describe('draft suite membership', () => { for (const [name, scenario] of scenarios) { if ( 'introducedIn' in scenario.source && - scenario.source.introducedIn === DRAFT_PROTOCOL_VERSION + scenario.source.introducedIn === DRAFT_SPEC_VERSION ) { expect( draftClientTesting.has(name), @@ -136,7 +195,7 @@ describe('draft suite membership', () => { for (const [name, scenario] of clientScenarios) { if ( 'introducedIn' in scenario.source && - scenario.source.introducedIn === DRAFT_PROTOCOL_VERSION + scenario.source.introducedIn === DRAFT_SPEC_VERSION ) { expect( draftServerTesting.has(name), @@ -146,16 +205,20 @@ describe('draft suite membership', () => { } }); - it('the draft suite covers the non-auth draft client scenarios', () => { + it('scenarios that shipped in 2026-07-28 are in the default suites, not draft', () => { const draft = new Set(listDraftScenarios()); - expect(draft.has('request-metadata')).toBe(true); - expect(draft.has('http-standard-headers')).toBe(true); - expect(draft.has('sep-2322-client-request-state')).toBe(true); + expect(draft.has('request-metadata')).toBe(false); + expect(draft.has('http-standard-headers')).toBe(false); + expect(draft.has('sep-2322-client-request-state')).toBe(false); + + const active = new Set(listActiveClientScenarios()); + expect(active.has('server-stateless')).toBe(true); + expect(active.has('caching')).toBe(true); + expect(active.has('input-required-result-basic-elicitation')).toBe(true); }); it('draft server-testing scenarios are excluded from the active suite', () => { const active = new Set(listActiveClientScenarios()); - expect(listDraftClientScenarios().length).toBeGreaterThan(0); for (const name of listDraftClientScenarios()) { expect( active.has(name), diff --git a/src/sdk-runner/config.ts b/src/sdk-runner/config.ts index edcd77a8..d7480d75 100644 --- a/src/sdk-runner/config.ts +++ b/src/sdk-runner/config.ts @@ -1,8 +1,9 @@ import { z } from 'zod'; import { - DRAFT_PROTOCOL_VERSION, - SPEC_VERSION_TIMELINE, - isSpecVersion + DATED_SPEC_VERSIONS, + isSpecVersion, + protocolVersionFor, + type DatedSpecVersion } from '../types'; // Fields an entry may vary per targeted spec version. Identity fields @@ -52,27 +53,26 @@ export const SdkConfigSchema = z.object({ // the flag isn't given (e.g. a v1 SDK pinned to the latest dated spec). // An explicit --spec-version on the sdk command always wins. specVersion: z.string().optional(), - // Per-spec-version defaults, keyed by the canonical spec version a run - // targets (--spec-version after alias resolution — so keys are the dated - // strings, never 'draft' — or the entry's own specVersion). Matched entries - // are merged over the base config field-by-field before CLI flags apply, so - // `sdk go-sdk --mode server --spec-version 2026-07-28` picks up the right - // server invocation with no manual overrides. + // Per-spec-version defaults, keyed by the dated spec version a run targets + // (--spec-version, a --requirements revision, or the entry's own + // specVersion; a `draft` run uses the entry for the draft's wire version). + // Matched entries are merged over the base config field-by-field before CLI + // flags apply, so `sdk go-sdk --mode server --spec-version 2026-07-28` picks + // up the right server invocation with no manual overrides. // Precedence: CLI flag > specOverrides > base entry. specOverrides: z .record(z.string(), SpecOverrideSchema) .superRefine((overrides, ctx) => { - // Keys must be canonical spec versions: a 'draft' key (or a typo'd - // date) would silently never match, because the requested version is - // resolved to its dated form before the lookup. + // Keys must be dated spec versions: a typo'd date would silently never + // match, and a 'draft' run is looked up by the draft's current wire + // version (DRAFT_PROTOCOL_VERSION) rather than the word 'draft'. for (const key of Object.keys(overrides)) { - if (!isSpecVersion(key)) { + if (!DATED_SPEC_VERSIONS.includes(key as DatedSpecVersion)) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: - `specOverrides key '${key}' is not a spec version. ` + - `Use one of: ${SPEC_VERSION_TIMELINE.join(', ')} ` + - `('draft' resolves to ${DRAFT_PROTOCOL_VERSION} before lookup, so key the dated form).` + `specOverrides key '${key}' is not a dated spec version. ` + + `Use one of: ${DATED_SPEC_VERSIONS.join(', ')}.` }); } } @@ -91,9 +91,13 @@ export function resolveConfigForSpec( config: SdkConfig, specVersion: string | undefined ): SdkConfig { - const override = specVersion - ? config.specOverrides?.[specVersion] - : undefined; + // Overlay keys are dated wire versions; a draft run resolves through the + // draft's current wire version so it reuses that revision's invocation. + const key = + specVersion && isSpecVersion(specVersion) + ? protocolVersionFor(specVersion) + : specVersion; + const override = key ? config.specOverrides?.[key] : undefined; if (!override) return config; const server = config.server || override.server diff --git a/src/sdk-runner/index.ts b/src/sdk-runner/index.ts index bcd5b20a..9d9ea2ac 100644 --- a/src/sdk-runner/index.ts +++ b/src/sdk-runner/index.ts @@ -223,7 +223,7 @@ export function createSdkCommand(): Command { // The targeted spec version (explicit flag wins over the per-SDK // default) selects that version's specOverrides entry, so version- // specific invocations live in config instead of copy-paste flags. - // 'draft' resolves to its dated alias so overlay keys stay canonical. + // A 'draft' run looks up the overlay for the draft's wire version. if ( options.requirements && (options.specVersion || options.suite || options.scenario) @@ -327,7 +327,7 @@ export function createSdkCommand(): Command { serverUrl, ...passThrough({ scenario: options.scenario, - // Default to the `active` suite (excludes pending/draft) — the same + // Default to the `active` suite (excludes pending and draft-only) — the same // suite tiering runs, and the most reasonable default to avoid // surfacing intentionally-deferred `pending` scenarios. suite: options.requirements diff --git a/src/sdk-runner/known-sdks.ts b/src/sdk-runner/known-sdks.ts index 843590d0..d65d1e46 100644 --- a/src/sdk-runner/known-sdks.ts +++ b/src/sdk-runner/known-sdks.ts @@ -22,8 +22,9 @@ export const KNOWN_SDKS: Record = { }, // v1.x — the published npm line. Same fixtures as v2; differs only in the // build (npm, not pnpm) and the baseline filename. Clones the typescript-sdk - // repo, defaulting to the `v1.x` branch. Targets the latest dated spec, so - // draft-only scenarios and checks are excluded by default. + // repo, defaulting to the `v1.x` branch. Pinned to 2025-11-25 (the last + // stateful revision it implements), so 2026-07-28 scenarios and checks are + // excluded by default. 'typescript-sdk-v1': { repo: 'typescript-sdk', defaultRef: 'v1.x', @@ -88,8 +89,8 @@ export const KNOWN_SDKS: Record = { }, // v1.x — the stable, published line of the python-sdk, analogous to // typescript-sdk-v1. Clones the python-sdk repo, defaulting to the `v1.x` - // branch, and targets the latest - // dated spec so draft-only scenarios/checks are excluded by default. uv + // branch, and is pinned to 2025-11-25 (the last stateful revision it + // implements) so 2026-07-28 scenarios/checks are excluded by default. uv // workspace: the `mcp` (client) and `mcp-everything-server` (server) packages // are both members, so one `uv sync --all-packages` covers both modes. // Fixtures live in the python-sdk repo (.github/actions/conformance/ and diff --git a/src/sdk-runner/sdk-runner.test.ts b/src/sdk-runner/sdk-runner.test.ts index 76fd7e51..7f71bcc5 100644 --- a/src/sdk-runner/sdk-runner.test.ts +++ b/src/sdk-runner/sdk-runner.test.ts @@ -1,4 +1,5 @@ import { describe, expect, it } from 'vitest'; +import { DRAFT_PROTOCOL_VERSION } from '../types'; import { parseSdkSpec } from './checkout'; import { lookupBuiltinConfig, KNOWN_SDKS } from './known-sdks'; import { SdkConfigSchema, resolveConfigForSpec } from './config'; @@ -167,9 +168,23 @@ describe('resolveConfigForSpec', () => { server: { command: 'x', url: 'http://localhost:3000' }, specOverrides: { draft: { server: { command: 'y' } } } }; - expect(() => SdkConfigSchema.parse(bad)).toThrow(/not a spec version/); + expect(() => SdkConfigSchema.parse(bad)).toThrow( + /not a dated spec version/ + ); const typo = { ...bad, specOverrides: { '2026-7-28': {} } }; - expect(() => SdkConfigSchema.parse(typo)).toThrow(/not a spec version/); + expect(() => SdkConfigSchema.parse(typo)).toThrow( + /not a dated spec version/ + ); + }); + + it("resolves a 'draft' run through the draft's wire version", () => { + // The draft currently shares 2026-07-28's wire string, so a draft run + // reuses that revision's invocation rather than matching nothing. + const resolved = resolveConfigForSpec(KNOWN_SDKS['go-sdk'], 'draft'); + expect(resolved.server?.command).toBe( + resolveConfigForSpec(KNOWN_SDKS['go-sdk'], DRAFT_PROTOCOL_VERSION).server + ?.command + ); }); it('returns the base config when no spec version is given', () => { diff --git a/src/spec-types/2025-06-18.schema.json b/src/spec-types/2025-06-18.schema.json index 775dc991..15c5d512 100644 --- a/src/spec-types/2025-06-18.schema.json +++ b/src/spec-types/2025-06-18.schema.json @@ -1399,10 +1399,10 @@ "type": "string" }, "maximum": { - "type": "integer" + "type": "number" }, "minimum": { - "type": "integer" + "type": "number" }, "title": { "type": "string" diff --git a/src/spec-types/2025-06-18.ts b/src/spec-types/2025-06-18.ts index 0df081ee..b7aa5245 100644 --- a/src/spec-types/2025-06-18.ts +++ b/src/spec-types/2025-06-18.ts @@ -1504,7 +1504,13 @@ export interface NumberSchema { type: "number" | "integer"; title?: string; description?: string; + /** + * @TJS-type number + */ minimum?: number; + /** + * @TJS-type number + */ maximum?: number; } diff --git a/src/spec-types/2025-11-25.schema.json b/src/spec-types/2025-11-25.schema.json index 9d2e662a..c722dd45 100644 --- a/src/spec-types/2025-11-25.schema.json +++ b/src/spec-types/2025-11-25.schema.json @@ -2107,16 +2107,16 @@ "NumberSchema": { "properties": { "default": { - "type": "integer" + "type": "number" }, "description": { "type": "string" }, "maximum": { - "type": "integer" + "type": "number" }, "minimum": { - "type": "integer" + "type": "number" }, "title": { "type": "string" diff --git a/src/spec-types/2025-11-25.ts b/src/spec-types/2025-11-25.ts index b734ae30..850a083f 100644 --- a/src/spec-types/2025-11-25.ts +++ b/src/spec-types/2025-11-25.ts @@ -2254,8 +2254,17 @@ export interface NumberSchema { type: "number" | "integer"; title?: string; description?: string; + /** + * @TJS-type number + */ minimum?: number; + /** + * @TJS-type number + */ maximum?: number; + /** + * @TJS-type number + */ default?: number; } diff --git a/src/spec-types/2026-07-28.schema.json b/src/spec-types/2026-07-28.schema.json new file mode 100644 index 00000000..213c58f6 --- /dev/null +++ b/src/spec-types/2026-07-28.schema.json @@ -0,0 +1,3963 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$defs": { + "Annotations": { + "description": "Optional annotations for the client. The client can use annotations to inform how objects are used or displayed", + "properties": { + "audience": { + "description": "Describes who the intended audience of this object or data is.\n\nIt can include multiple entries to indicate content useful for multiple audiences (e.g., `[\"user\", \"assistant\"]`).", + "items": { + "$ref": "#/$defs/Role" + }, + "type": "array" + }, + "lastModified": { + "description": "The moment the resource was last modified, as an ISO 8601 formatted string.\n\nShould be an ISO 8601 formatted string (e.g., \"2025-01-12T15:00:58Z\").\n\nExamples: last activity timestamp in an open file, timestamp when the resource\nwas attached, etc.", + "type": "string" + }, + "priority": { + "description": "Describes how important this data is for operating the server.\n\nA value of 1 means \"most important,\" and indicates that the data is\neffectively required, while 0 means \"least important,\" and indicates that\nthe data is entirely optional.", + "maximum": 1, + "minimum": 0, + "type": "number" + } + }, + "type": "object" + }, + "AudioContent": { + "description": "Audio provided to or from an LLM.", + "properties": { + "_meta": { + "$ref": "#/$defs/MetaObject" + }, + "annotations": { + "$ref": "#/$defs/Annotations", + "description": "Optional annotations for the client." + }, + "data": { + "description": "The base64-encoded audio data.", + "format": "byte", + "type": "string" + }, + "mimeType": { + "description": "The MIME type of the audio. Different providers may support different audio types.", + "type": "string" + }, + "type": { + "const": "audio", + "type": "string" + } + }, + "required": [ + "data", + "mimeType", + "type" + ], + "type": "object" + }, + "BaseMetadata": { + "description": "Base interface for metadata with name (identifier) and title (display name) properties.", + "properties": { + "name": { + "description": "Intended for programmatic or logical use, but used as a display name in past specs or fallback (if title isn't present).", + "type": "string" + }, + "title": { + "description": "Intended for UI and end-user contexts — optimized to be human-readable and easily understood,\neven by those unfamiliar with domain-specific terminology.\n\nIf not provided, the name should be used for display (except for {@link Tool},\nwhere `annotations.title` should be given precedence over using `name`,\nif present).", + "type": "string" + } + }, + "required": [ + "name" + ], + "type": "object" + }, + "BlobResourceContents": { + "properties": { + "_meta": { + "$ref": "#/$defs/MetaObject" + }, + "blob": { + "description": "A base64-encoded string representing the binary data of the item.", + "format": "byte", + "type": "string" + }, + "mimeType": { + "description": "The MIME type of this resource, if known.", + "type": "string" + }, + "uri": { + "description": "The URI of this resource.", + "format": "uri", + "type": "string" + } + }, + "required": [ + "blob", + "uri" + ], + "type": "object" + }, + "BooleanSchema": { + "properties": { + "default": { + "type": "boolean" + }, + "description": { + "type": "string" + }, + "title": { + "type": "string" + }, + "type": { + "const": "boolean", + "type": "string" + } + }, + "required": [ + "type" + ], + "type": "object" + }, + "CacheableResult": { + "description": "A result that supports a time-to-live (TTL) hint for client-side caching.", + "properties": { + "_meta": { + "$ref": "#/$defs/ResultMetaObject" + }, + "cacheScope": { + "description": "Indicates the intended scope of the cached response, analogous to HTTP\n`Cache-Control: public` vs `Cache-Control: private`.\n\n- `\"public\"`: The response does not contain user-specific data. Any\n client or intermediary (e.g., shared gateway, caching proxy) MAY cache\n the response and serve it across authorization contexts.\n- `\"private\"`: The response MAY be cached and reused only within the\n same authorization context. Caches MUST NOT be shared across\n authorization contexts (e.g., a different access token requires a\n different cache).", + "enum": [ + "private", + "public" + ], + "type": "string" + }, + "resultType": { + "description": "Indicates the type of the result, which allows the client to determine\nhow to parse the result object.\n\nServers implementing this protocol version MUST include this field.\nFor backward compatibility, when a client receives a result from a\nserver implementing an earlier protocol version (which does not include\n`resultType`), the client MUST treat the absent field as `\"complete\"`.", + "type": "string" + }, + "ttlMs": { + "description": "A hint from the server indicating how long (in milliseconds) the\nclient MAY cache this response before re-fetching. Semantics are\nanalogous to HTTP Cache-Control max-age.\n\n- If 0, The response SHOULD be considered immediately stale,\n The client MAY re-fetch every time the result is needed.\n- If positive, the client SHOULD consider the result fresh for this many\n milliseconds after receiving the response.", + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "cacheScope", + "resultType", + "ttlMs" + ], + "type": "object" + }, + "CallToolRequest": { + "description": "Used by the client to invoke a tool provided by the server.", + "properties": { + "id": { + "$ref": "#/$defs/RequestId" + }, + "jsonrpc": { + "const": "2.0", + "type": "string" + }, + "method": { + "const": "tools/call", + "type": "string" + }, + "params": { + "$ref": "#/$defs/CallToolRequestParams" + } + }, + "required": [ + "id", + "jsonrpc", + "method", + "params" + ], + "type": "object" + }, + "CallToolRequestParams": { + "description": "Parameters for a `tools/call` request.", + "properties": { + "_meta": { + "$ref": "#/$defs/RequestMetaObject" + }, + "arguments": { + "additionalProperties": {}, + "description": "Arguments to use for the tool call.", + "type": "object" + }, + "inputResponses": { + "$ref": "#/$defs/InputResponses" + }, + "name": { + "description": "The name of the tool.", + "type": "string" + }, + "requestState": { + "type": "string" + } + }, + "required": [ + "_meta", + "name" + ], + "type": "object" + }, + "CallToolResult": { + "description": "The result returned by the server for a {@link CallToolRequesttools/call} request.", + "properties": { + "_meta": { + "$ref": "#/$defs/ResultMetaObject" + }, + "content": { + "description": "A list of content objects that represent the unstructured result of the tool call.", + "items": { + "$ref": "#/$defs/ContentBlock" + }, + "type": "array" + }, + "isError": { + "description": "Whether the tool call ended in an error.\n\nIf not set, this is assumed to be false (the call was successful).\n\nAny errors that originate from the tool SHOULD be reported inside the result\nobject, with `isError` set to true, _not_ as an MCP protocol-level error\nresponse. Otherwise, the LLM would not be able to see that an error occurred\nand self-correct.\n\nHowever, any errors in _finding_ the tool, an error indicating that the\nserver does not support tool calls, or any other exceptional conditions,\nshould be reported as an MCP error response.", + "type": "boolean" + }, + "resultType": { + "description": "Indicates the type of the result, which allows the client to determine\nhow to parse the result object.\n\nServers implementing this protocol version MUST include this field.\nFor backward compatibility, when a client receives a result from a\nserver implementing an earlier protocol version (which does not include\n`resultType`), the client MUST treat the absent field as `\"complete\"`.", + "type": "string" + }, + "structuredContent": { + "description": "An optional JSON value that represents the structured result of the tool call.\n\nThis can be any JSON value (object, array, string, number, boolean, or null)\nthat conforms to the tool's outputSchema if one is defined." + } + }, + "required": [ + "content", + "resultType" + ], + "type": "object" + }, + "CallToolResultResponse": { + "description": "A successful response from the server for a {@link CallToolRequesttools/call} request.", + "properties": { + "id": { + "$ref": "#/$defs/RequestId" + }, + "jsonrpc": { + "const": "2.0", + "type": "string" + }, + "result": { + "anyOf": [ + { + "$ref": "#/$defs/InputRequiredResult" + }, + { + "$ref": "#/$defs/CallToolResult" + } + ] + } + }, + "required": [ + "id", + "jsonrpc", + "result" + ], + "type": "object" + }, + "CancelledNotification": { + "description": "This notification is sent by the client to indicate that it is cancelling a request it previously issued.\n\nOn stdio, the server also sends this notification, solely to terminate a {@link SubscriptionsListenRequestsubscriptions/listen} stream: it references the ID of the `subscriptions/listen` request that opened the stream. Servers MUST NOT use this notification to cancel any other request.\n\nThe request SHOULD still be in-flight, but due to communication latency, it is always possible that this notification MAY arrive after the request has already finished.\n\nThis notification indicates that the result will be unused, so any associated processing SHOULD cease.", + "properties": { + "jsonrpc": { + "const": "2.0", + "type": "string" + }, + "method": { + "const": "notifications/cancelled", + "type": "string" + }, + "params": { + "$ref": "#/$defs/CancelledNotificationParams" + } + }, + "required": [ + "jsonrpc", + "method", + "params" + ], + "type": "object" + }, + "CancelledNotificationParams": { + "description": "Parameters for a `notifications/cancelled` notification.", + "properties": { + "_meta": { + "$ref": "#/$defs/NotificationMetaObject" + }, + "reason": { + "description": "An optional string describing the reason for the cancellation. This MAY be logged or presented to the user.", + "type": "string" + }, + "requestId": { + "$ref": "#/$defs/RequestId", + "description": "The ID of the request to cancel.\n\nThis MUST correspond to the ID of a request the client previously issued." + } + }, + "required": [ + "requestId" + ], + "type": "object" + }, + "ClientCapabilities": { + "description": "Capabilities a client may support. Known capabilities are defined here, in this schema, but this is not a closed set: any client can define its own, additional capabilities.", + "properties": { + "elicitation": { + "description": "Present if the client supports elicitation from the server.", + "properties": { + "form": { + "$ref": "#/$defs/JSONObject" + }, + "url": { + "$ref": "#/$defs/JSONObject" + } + }, + "type": "object" + }, + "experimental": { + "additionalProperties": { + "$ref": "#/$defs/JSONObject" + }, + "description": "Experimental, non-standard capabilities that the client supports.", + "type": "object" + }, + "extensions": { + "additionalProperties": { + "$ref": "#/$defs/JSONObject" + }, + "description": "Optional MCP extensions that the client supports. Keys are extension identifiers\n(e.g., \"io.modelcontextprotocol/oauth-client-credentials\"), and values are\nper-extension settings objects. An empty object indicates support with no settings.\n\nKeys MUST follow the {@link MetaObject`_meta` key naming rules}, with a\nmandatory prefix.", + "type": "object" + }, + "roots": { + "description": "Present if the client supports listing roots.", + "properties": {}, + "type": "object" + }, + "sampling": { + "description": "Present if the client supports sampling from an LLM.", + "properties": { + "context": { + "$ref": "#/$defs/JSONObject", + "description": "Whether the client supports context inclusion via `includeContext` parameter.\nIf not declared, servers SHOULD only use `includeContext: \"none\"` (or omit it)." + }, + "tools": { + "$ref": "#/$defs/JSONObject", + "description": "Whether the client supports tool use via `tools` and `toolChoice` parameters." + } + }, + "type": "object" + } + }, + "type": "object" + }, + "ClientNotification": { + "description": "This notification is sent by the client to indicate that it is cancelling a request it previously issued.\n\nOn stdio, the server also sends this notification, solely to terminate a {@link SubscriptionsListenRequestsubscriptions/listen} stream: it references the ID of the `subscriptions/listen` request that opened the stream. Servers MUST NOT use this notification to cancel any other request.\n\nThe request SHOULD still be in-flight, but due to communication latency, it is always possible that this notification MAY arrive after the request has already finished.\n\nThis notification indicates that the result will be unused, so any associated processing SHOULD cease.", + "properties": { + "jsonrpc": { + "const": "2.0", + "type": "string" + }, + "method": { + "const": "notifications/cancelled", + "type": "string" + }, + "params": { + "$ref": "#/$defs/CancelledNotificationParams" + } + }, + "required": [ + "jsonrpc", + "method", + "params" + ], + "type": "object" + }, + "ClientRequest": { + "anyOf": [ + { + "$ref": "#/$defs/DiscoverRequest" + }, + { + "$ref": "#/$defs/ListResourcesRequest" + }, + { + "$ref": "#/$defs/ListResourceTemplatesRequest" + }, + { + "$ref": "#/$defs/ReadResourceRequest" + }, + { + "$ref": "#/$defs/SubscriptionsListenRequest" + }, + { + "$ref": "#/$defs/ListPromptsRequest" + }, + { + "$ref": "#/$defs/GetPromptRequest" + }, + { + "$ref": "#/$defs/ListToolsRequest" + }, + { + "$ref": "#/$defs/CallToolRequest" + }, + { + "$ref": "#/$defs/CompleteRequest" + } + ] + }, + "ClientResult": { + "$ref": "#/$defs/Result", + "description": "Common result fields." + }, + "CompleteRequest": { + "description": "A request from the client to the server, to ask for completion options.", + "properties": { + "id": { + "$ref": "#/$defs/RequestId" + }, + "jsonrpc": { + "const": "2.0", + "type": "string" + }, + "method": { + "const": "completion/complete", + "type": "string" + }, + "params": { + "$ref": "#/$defs/CompleteRequestParams" + } + }, + "required": [ + "id", + "jsonrpc", + "method", + "params" + ], + "type": "object" + }, + "CompleteRequestParams": { + "description": "Parameters for a `completion/complete` request.", + "properties": { + "_meta": { + "$ref": "#/$defs/RequestMetaObject" + }, + "argument": { + "description": "The argument's information", + "properties": { + "name": { + "description": "The name of the argument", + "type": "string" + }, + "value": { + "description": "The value of the argument to use for completion matching.", + "type": "string" + } + }, + "required": [ + "name", + "value" + ], + "type": "object" + }, + "context": { + "description": "Additional, optional context for completions", + "properties": { + "arguments": { + "additionalProperties": { + "type": "string" + }, + "description": "Previously-resolved variables in a URI template or prompt.", + "type": "object" + } + }, + "type": "object" + }, + "ref": { + "anyOf": [ + { + "$ref": "#/$defs/PromptReference" + }, + { + "$ref": "#/$defs/ResourceTemplateReference" + } + ] + } + }, + "required": [ + "_meta", + "argument", + "ref" + ], + "type": "object" + }, + "CompleteResult": { + "description": "The result returned by the server for a {@link CompleteRequestcompletion/complete} request.", + "properties": { + "_meta": { + "$ref": "#/$defs/ResultMetaObject" + }, + "completion": { + "properties": { + "hasMore": { + "description": "Indicates whether there are additional completion options beyond those provided in the current response, even if the exact total is unknown.", + "type": "boolean" + }, + "total": { + "description": "The total number of completion options available. This can exceed the number of values actually sent in the response.", + "type": "integer" + }, + "values": { + "description": "An array of completion values. Must not exceed 100 items.", + "items": { + "type": "string" + }, + "maxItems": 100, + "type": "array" + } + }, + "required": [ + "values" + ], + "type": "object" + }, + "resultType": { + "description": "Indicates the type of the result, which allows the client to determine\nhow to parse the result object.\n\nServers implementing this protocol version MUST include this field.\nFor backward compatibility, when a client receives a result from a\nserver implementing an earlier protocol version (which does not include\n`resultType`), the client MUST treat the absent field as `\"complete\"`.", + "type": "string" + } + }, + "required": [ + "completion", + "resultType" + ], + "type": "object" + }, + "CompleteResultResponse": { + "description": "A successful response from the server for a {@link CompleteRequestcompletion/complete} request.", + "properties": { + "id": { + "$ref": "#/$defs/RequestId" + }, + "jsonrpc": { + "const": "2.0", + "type": "string" + }, + "result": { + "$ref": "#/$defs/CompleteResult" + } + }, + "required": [ + "id", + "jsonrpc", + "result" + ], + "type": "object" + }, + "ContentBlock": { + "anyOf": [ + { + "$ref": "#/$defs/TextContent" + }, + { + "$ref": "#/$defs/ImageContent" + }, + { + "$ref": "#/$defs/AudioContent" + }, + { + "$ref": "#/$defs/ResourceLink" + }, + { + "$ref": "#/$defs/EmbeddedResource" + } + ] + }, + "CreateMessageRequest": { + "description": "A request from the server to sample an LLM via the client. The client has full discretion over which model to select. The client should also inform the user before beginning sampling, to allow them to inspect the request (human in the loop) and decide whether to approve it.", + "properties": { + "method": { + "const": "sampling/createMessage", + "type": "string" + }, + "params": { + "$ref": "#/$defs/CreateMessageRequestParams" + } + }, + "required": [ + "method", + "params" + ], + "type": "object" + }, + "CreateMessageRequestParams": { + "description": "Parameters for a `sampling/createMessage` request.", + "properties": { + "includeContext": { + "description": "A request to include context from one or more MCP servers (including the caller), to be attached to the prompt.\nThe client MAY ignore this request.\n\nDefault is `\"none\"`. The values `\"thisServer\"` and `\"allServers\"` are deprecated (SEP-2596): servers SHOULD\nomit this field or use `\"none\"`, and SHOULD only use the deprecated values if the client declares\n{@link ClientCapabilities.sampling.context}.", + "enum": [ + "allServers", + "none", + "thisServer" + ], + "type": "string" + }, + "maxTokens": { + "description": "The requested maximum number of tokens to sample (to prevent runaway completions).\n\nThe client MAY choose to sample fewer tokens than the requested maximum.", + "type": "integer" + }, + "messages": { + "items": { + "$ref": "#/$defs/SamplingMessage" + }, + "type": "array" + }, + "metadata": { + "$ref": "#/$defs/JSONObject", + "description": "Optional metadata to pass through to the LLM provider. The format of this metadata is provider-specific." + }, + "modelPreferences": { + "$ref": "#/$defs/ModelPreferences", + "description": "The server's preferences for which model to select. The client MAY ignore these preferences." + }, + "stopSequences": { + "items": { + "type": "string" + }, + "type": "array" + }, + "systemPrompt": { + "description": "An optional system prompt the server wants to use for sampling. The client MAY modify or omit this prompt.", + "type": "string" + }, + "temperature": { + "type": "number" + }, + "toolChoice": { + "$ref": "#/$defs/ToolChoice", + "description": "Controls how the model uses tools.\nThe client MUST return an error if this field is provided but {@link ClientCapabilities.sampling.tools} is not declared.\nDefault is `{ mode: \"auto\" }`." + }, + "tools": { + "description": "Tools that the model may use during generation.\nThe client MUST return an error if this field is provided but {@link ClientCapabilities.sampling.tools} is not declared.", + "items": { + "$ref": "#/$defs/Tool" + }, + "type": "array" + } + }, + "required": [ + "maxTokens", + "messages" + ], + "type": "object" + }, + "CreateMessageResult": { + "description": "The result returned by the client for a {@link CreateMessageRequestsampling/createMessage} request.\nThe client should inform the user before returning the sampled message, to allow them\nto inspect the response (human in the loop) and decide whether to allow the server to see it.", + "properties": { + "_meta": { + "$ref": "#/$defs/MetaObject" + }, + "content": { + "anyOf": [ + { + "$ref": "#/$defs/TextContent" + }, + { + "$ref": "#/$defs/ImageContent" + }, + { + "$ref": "#/$defs/AudioContent" + }, + { + "$ref": "#/$defs/ToolUseContent" + }, + { + "$ref": "#/$defs/ToolResultContent" + }, + { + "items": { + "$ref": "#/$defs/SamplingMessageContentBlock" + }, + "type": "array" + } + ] + }, + "model": { + "description": "The name of the model that generated the message.", + "type": "string" + }, + "role": { + "$ref": "#/$defs/Role" + }, + "stopReason": { + "description": "The reason why sampling stopped, if known.\n\nStandard values:\n- `\"endTurn\"`: Natural end of the assistant's turn\n- `\"stopSequence\"`: A stop sequence was encountered\n- `\"maxTokens\"`: Maximum token limit was reached\n- `\"toolUse\"`: The model wants to use one or more tools\n\nThis field is an open string to allow for provider-specific stop reasons.", + "type": "string" + } + }, + "required": [ + "content", + "model", + "role" + ], + "type": "object" + }, + "Cursor": { + "description": "An opaque token used to represent a cursor for pagination.", + "type": "string" + }, + "DiscoverRequest": { + "description": "A request from the client asking the server to advertise its supported\nprotocol versions, capabilities, and other metadata. Servers **MUST**\nimplement `server/discover`. Clients **MAY** call it but are not required\nto — version negotiation can also happen inline via per-request `_meta`.", + "properties": { + "id": { + "$ref": "#/$defs/RequestId" + }, + "jsonrpc": { + "const": "2.0", + "type": "string" + }, + "method": { + "const": "server/discover", + "type": "string" + }, + "params": { + "$ref": "#/$defs/RequestParams" + } + }, + "required": [ + "id", + "jsonrpc", + "method", + "params" + ], + "type": "object" + }, + "DiscoverResult": { + "description": "The result returned by the server for a {@link DiscoverRequestserver/discover} request.", + "properties": { + "_meta": { + "$ref": "#/$defs/ResultMetaObject" + }, + "cacheScope": { + "description": "Indicates the intended scope of the cached response, analogous to HTTP\n`Cache-Control: public` vs `Cache-Control: private`.\n\n- `\"public\"`: The response does not contain user-specific data. Any\n client or intermediary (e.g., shared gateway, caching proxy) MAY cache\n the response and serve it across authorization contexts.\n- `\"private\"`: The response MAY be cached and reused only within the\n same authorization context. Caches MUST NOT be shared across\n authorization contexts (e.g., a different access token requires a\n different cache).", + "enum": [ + "private", + "public" + ], + "type": "string" + }, + "capabilities": { + "$ref": "#/$defs/ServerCapabilities", + "description": "The capabilities of the server." + }, + "instructions": { + "description": "Natural-language guidance describing the server and its features.\n\nThis can be used by clients to improve an LLM's understanding of\navailable tools (e.g., by including it in a system prompt). It should\nfocus on information that helps the model use the server effectively\nand should not duplicate information already in tool descriptions.", + "type": "string" + }, + "resultType": { + "description": "Indicates the type of the result, which allows the client to determine\nhow to parse the result object.\n\nServers implementing this protocol version MUST include this field.\nFor backward compatibility, when a client receives a result from a\nserver implementing an earlier protocol version (which does not include\n`resultType`), the client MUST treat the absent field as `\"complete\"`.", + "type": "string" + }, + "supportedVersions": { + "description": "MCP Protocol Versions this server supports. The client should choose a\nversion from this list for use in subsequent requests.", + "items": { + "type": "string" + }, + "type": "array" + }, + "ttlMs": { + "description": "A hint from the server indicating how long (in milliseconds) the\nclient MAY cache this response before re-fetching. Semantics are\nanalogous to HTTP Cache-Control max-age.\n\n- If 0, The response SHOULD be considered immediately stale,\n The client MAY re-fetch every time the result is needed.\n- If positive, the client SHOULD consider the result fresh for this many\n milliseconds after receiving the response.", + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "cacheScope", + "capabilities", + "resultType", + "supportedVersions", + "ttlMs" + ], + "type": "object" + }, + "DiscoverResultResponse": { + "description": "A successful response from the server for a {@link DiscoverRequestserver/discover} request.", + "properties": { + "id": { + "$ref": "#/$defs/RequestId" + }, + "jsonrpc": { + "const": "2.0", + "type": "string" + }, + "result": { + "$ref": "#/$defs/DiscoverResult" + } + }, + "required": [ + "id", + "jsonrpc", + "result" + ], + "type": "object" + }, + "ElicitRequest": { + "description": "A request from the server to elicit additional information from the user via the client.", + "properties": { + "method": { + "const": "elicitation/create", + "type": "string" + }, + "params": { + "$ref": "#/$defs/ElicitRequestParams" + } + }, + "required": [ + "method", + "params" + ], + "type": "object" + }, + "ElicitRequestFormParams": { + "description": "The parameters for a request to elicit non-sensitive information from the user via a form in the client.", + "properties": { + "message": { + "description": "The message to present to the user describing what information is being requested.", + "type": "string" + }, + "mode": { + "const": "form", + "description": "The elicitation mode.", + "type": "string" + }, + "requestedSchema": { + "description": "A restricted subset of JSON Schema.\nOnly top-level properties are allowed, without nesting.", + "properties": { + "$schema": { + "type": "string" + }, + "properties": { + "additionalProperties": { + "$ref": "#/$defs/PrimitiveSchemaDefinition" + }, + "type": "object" + }, + "required": { + "items": { + "type": "string" + }, + "type": "array" + }, + "type": { + "const": "object", + "type": "string" + } + }, + "required": [ + "properties", + "type" + ], + "type": "object" + } + }, + "required": [ + "message", + "requestedSchema" + ], + "type": "object" + }, + "ElicitRequestParams": { + "anyOf": [ + { + "$ref": "#/$defs/ElicitRequestFormParams" + }, + { + "$ref": "#/$defs/ElicitRequestURLParams" + } + ], + "description": "The parameters for a request to elicit additional information from the user via the client." + }, + "ElicitRequestURLParams": { + "description": "The parameters for a request to elicit information from the user via a URL in the client.", + "properties": { + "message": { + "description": "The message to present to the user explaining why the interaction is needed.", + "type": "string" + }, + "mode": { + "const": "url", + "description": "The elicitation mode.", + "type": "string" + }, + "url": { + "description": "The URL that the user should navigate to.", + "format": "uri", + "type": "string" + } + }, + "required": [ + "message", + "mode", + "url" + ], + "type": "object" + }, + "ElicitResult": { + "description": "The result returned by the client for an {@link ElicitRequestelicitation/create} request.", + "properties": { + "action": { + "description": "The user action in response to the elicitation.\n- `\"accept\"`: User submitted the form/confirmed the action\n- `\"decline\"`: User explicitly declined the action\n- `\"cancel\"`: User dismissed without making an explicit choice", + "enum": [ + "accept", + "cancel", + "decline" + ], + "type": "string" + }, + "content": { + "additionalProperties": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": [ + "string", + "integer", + "boolean" + ] + } + ] + }, + "description": "The submitted form data, only present when action is `\"accept\"` and mode was `\"form\"`.\nContains values matching the requested schema.\nOmitted for out-of-band mode responses.", + "type": "object" + } + }, + "required": [ + "action" + ], + "type": "object" + }, + "EmbeddedResource": { + "description": "The contents of a resource, embedded into a prompt or tool call result.\n\nIt is up to the client how best to render embedded resources for the benefit\nof the LLM and/or the user.", + "properties": { + "_meta": { + "$ref": "#/$defs/MetaObject" + }, + "annotations": { + "$ref": "#/$defs/Annotations", + "description": "Optional annotations for the client." + }, + "resource": { + "anyOf": [ + { + "$ref": "#/$defs/TextResourceContents" + }, + { + "$ref": "#/$defs/BlobResourceContents" + } + ] + }, + "type": { + "const": "resource", + "type": "string" + } + }, + "required": [ + "resource", + "type" + ], + "type": "object" + }, + "EmptyResult": { + "$ref": "#/$defs/Result", + "description": "Common result fields." + }, + "EnumSchema": { + "anyOf": [ + { + "$ref": "#/$defs/UntitledSingleSelectEnumSchema" + }, + { + "$ref": "#/$defs/TitledSingleSelectEnumSchema" + }, + { + "$ref": "#/$defs/UntitledMultiSelectEnumSchema" + }, + { + "$ref": "#/$defs/TitledMultiSelectEnumSchema" + }, + { + "$ref": "#/$defs/LegacyTitledEnumSchema" + } + ] + }, + "Error": { + "properties": { + "code": { + "description": "The error type that occurred.", + "type": "integer" + }, + "data": { + "description": "Additional information about the error. The value of this member is defined by the sender (e.g. detailed error information, nested errors etc.)." + }, + "message": { + "description": "A short description of the error. The message SHOULD be limited to a concise single sentence.", + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + "GetPromptRequest": { + "description": "Used by the client to get a prompt provided by the server.", + "properties": { + "id": { + "$ref": "#/$defs/RequestId" + }, + "jsonrpc": { + "const": "2.0", + "type": "string" + }, + "method": { + "const": "prompts/get", + "type": "string" + }, + "params": { + "$ref": "#/$defs/GetPromptRequestParams" + } + }, + "required": [ + "id", + "jsonrpc", + "method", + "params" + ], + "type": "object" + }, + "GetPromptRequestParams": { + "description": "Parameters for a `prompts/get` request.", + "properties": { + "_meta": { + "$ref": "#/$defs/RequestMetaObject" + }, + "arguments": { + "additionalProperties": { + "type": "string" + }, + "description": "Arguments to use for templating the prompt.", + "type": "object" + }, + "inputResponses": { + "$ref": "#/$defs/InputResponses" + }, + "name": { + "description": "The name of the prompt or prompt template.", + "type": "string" + }, + "requestState": { + "type": "string" + } + }, + "required": [ + "_meta", + "name" + ], + "type": "object" + }, + "GetPromptResult": { + "description": "The result returned by the server for a {@link GetPromptRequestprompts/get} request.", + "properties": { + "_meta": { + "$ref": "#/$defs/ResultMetaObject" + }, + "description": { + "description": "An optional description for the prompt.", + "type": "string" + }, + "messages": { + "items": { + "$ref": "#/$defs/PromptMessage" + }, + "type": "array" + }, + "resultType": { + "description": "Indicates the type of the result, which allows the client to determine\nhow to parse the result object.\n\nServers implementing this protocol version MUST include this field.\nFor backward compatibility, when a client receives a result from a\nserver implementing an earlier protocol version (which does not include\n`resultType`), the client MUST treat the absent field as `\"complete\"`.", + "type": "string" + } + }, + "required": [ + "messages", + "resultType" + ], + "type": "object" + }, + "GetPromptResultResponse": { + "description": "A successful response from the server for a {@link GetPromptRequestprompts/get} request.", + "properties": { + "id": { + "$ref": "#/$defs/RequestId" + }, + "jsonrpc": { + "const": "2.0", + "type": "string" + }, + "result": { + "anyOf": [ + { + "$ref": "#/$defs/InputRequiredResult" + }, + { + "$ref": "#/$defs/GetPromptResult" + } + ] + } + }, + "required": [ + "id", + "jsonrpc", + "result" + ], + "type": "object" + }, + "HeaderMismatchError": { + "description": "Returned when a server rejects a request because the values in the HTTP\nheaders do not match the corresponding values in the request body, or\nbecause required headers are missing or malformed. For HTTP, the response\nstatus code MUST be `400 Bad Request`.", + "properties": { + "error": { + "allOf": [ + { + "$ref": "#/$defs/Error" + }, + { + "properties": { + "code": { + "const": -32020, + "type": "integer" + } + }, + "required": [ + "code" + ], + "type": "object" + } + ] + }, + "id": { + "$ref": "#/$defs/RequestId" + }, + "jsonrpc": { + "const": "2.0", + "type": "string" + } + }, + "required": [ + "error", + "jsonrpc" + ], + "type": "object" + }, + "Icon": { + "description": "An optionally-sized icon that can be displayed in a user interface.", + "properties": { + "mimeType": { + "description": "Optional MIME type override if the source MIME type is missing or generic.\nFor example: `\"image/png\"`, `\"image/jpeg\"`, or `\"image/svg+xml\"`.", + "type": "string" + }, + "sizes": { + "description": "Optional array of strings that specify sizes at which the icon can be used.\nEach string should be in WxH format (e.g., `\"48x48\"`, `\"96x96\"`) or `\"any\"` for scalable formats like SVG.\n\nIf not provided, the client should assume that the icon can be used at any size.", + "items": { + "type": "string" + }, + "type": "array" + }, + "src": { + "description": "A standard URI pointing to an icon resource. May be an HTTP/HTTPS URL or a\n`data:` URI with Base64-encoded image data.\n\nConsumers SHOULD take steps to ensure URLs serving icons are from the\nsame domain as the client/server or a trusted domain.\n\nConsumers SHOULD take appropriate precautions when consuming SVGs as they can contain\nexecutable JavaScript.", + "format": "uri", + "type": "string" + }, + "theme": { + "description": "Optional specifier for the theme this icon is designed for. `\"light\"` indicates\nthe icon is designed to be used with a light background, and `\"dark\"` indicates\nthe icon is designed to be used with a dark background.\n\nIf not provided, the client should assume the icon can be used with any theme.", + "enum": [ + "dark", + "light" + ], + "type": "string" + } + }, + "required": [ + "src" + ], + "type": "object" + }, + "Icons": { + "description": "Base interface to add `icons` property.", + "properties": { + "icons": { + "description": "Optional set of sized icons that the client can display in a user interface.\n\nClients that support rendering icons MUST support at least the following MIME types:\n- `image/png` - PNG images (safe, universal compatibility)\n- `image/jpeg` (and `image/jpg`) - JPEG images (safe, universal compatibility)\n\nClients that support rendering icons SHOULD also support:\n- `image/svg+xml` - SVG images (scalable but requires security precautions)\n- `image/webp` - WebP images (modern, efficient format)", + "items": { + "$ref": "#/$defs/Icon" + }, + "type": "array" + } + }, + "type": "object" + }, + "ImageContent": { + "description": "An image provided to or from an LLM.", + "properties": { + "_meta": { + "$ref": "#/$defs/MetaObject" + }, + "annotations": { + "$ref": "#/$defs/Annotations", + "description": "Optional annotations for the client." + }, + "data": { + "description": "The base64-encoded image data.", + "format": "byte", + "type": "string" + }, + "mimeType": { + "description": "The MIME type of the image. Different providers may support different image types.", + "type": "string" + }, + "type": { + "const": "image", + "type": "string" + } + }, + "required": [ + "data", + "mimeType", + "type" + ], + "type": "object" + }, + "Implementation": { + "description": "Describes the MCP implementation.", + "properties": { + "description": { + "description": "An optional human-readable description of what this implementation does.\n\nThis can be used by clients or servers to provide context about their purpose\nand capabilities. For example, a server might describe the types of resources\nor tools it provides, while a client might describe its intended use case.", + "type": "string" + }, + "icons": { + "description": "Optional set of sized icons that the client can display in a user interface.\n\nClients that support rendering icons MUST support at least the following MIME types:\n- `image/png` - PNG images (safe, universal compatibility)\n- `image/jpeg` (and `image/jpg`) - JPEG images (safe, universal compatibility)\n\nClients that support rendering icons SHOULD also support:\n- `image/svg+xml` - SVG images (scalable but requires security precautions)\n- `image/webp` - WebP images (modern, efficient format)", + "items": { + "$ref": "#/$defs/Icon" + }, + "type": "array" + }, + "name": { + "description": "Intended for programmatic or logical use, but used as a display name in past specs or fallback (if title isn't present).", + "type": "string" + }, + "title": { + "description": "Intended for UI and end-user contexts — optimized to be human-readable and easily understood,\neven by those unfamiliar with domain-specific terminology.\n\nIf not provided, the name should be used for display (except for {@link Tool},\nwhere `annotations.title` should be given precedence over using `name`,\nif present).", + "type": "string" + }, + "version": { + "description": "The version of this implementation.", + "type": "string" + }, + "websiteUrl": { + "description": "An optional URL of the website for this implementation.", + "format": "uri", + "type": "string" + } + }, + "required": [ + "name", + "version" + ], + "type": "object" + }, + "InputRequest": { + "anyOf": [ + { + "$ref": "#/$defs/CreateMessageRequest" + }, + { + "$ref": "#/$defs/ListRootsRequest" + }, + { + "$ref": "#/$defs/ElicitRequest" + } + ] + }, + "InputRequests": { + "additionalProperties": { + "$ref": "#/$defs/InputRequest" + }, + "description": "A map of server-initiated requests that the client must fulfill.\nKeys are server-assigned identifiers; values are the request objects.", + "type": "object" + }, + "InputRequiredResult": { + "description": "An InputRequiredResult sent by the server to indicate that additional input is needed\nbefore the request can be completed.\n\nAt least one of `inputRequests` or `requestState` MUST be present.", + "properties": { + "_meta": { + "$ref": "#/$defs/ResultMetaObject" + }, + "inputRequests": { + "$ref": "#/$defs/InputRequests" + }, + "requestState": { + "type": "string" + }, + "resultType": { + "description": "Indicates the type of the result, which allows the client to determine\nhow to parse the result object.\n\nServers implementing this protocol version MUST include this field.\nFor backward compatibility, when a client receives a result from a\nserver implementing an earlier protocol version (which does not include\n`resultType`), the client MUST treat the absent field as `\"complete\"`.", + "type": "string" + } + }, + "required": [ + "resultType" + ], + "type": "object" + }, + "InputResponse": { + "anyOf": [ + { + "$ref": "#/$defs/CreateMessageResult" + }, + { + "$ref": "#/$defs/ListRootsResult" + }, + { + "$ref": "#/$defs/ElicitResult" + } + ] + }, + "InputResponseRequestParams": { + "properties": { + "_meta": { + "$ref": "#/$defs/RequestMetaObject" + }, + "inputResponses": { + "$ref": "#/$defs/InputResponses" + }, + "requestState": { + "type": "string" + } + }, + "required": [ + "_meta" + ], + "type": "object" + }, + "InputResponses": { + "additionalProperties": { + "$ref": "#/$defs/InputResponse" + }, + "description": "A map of client responses to server-initiated requests.\nKeys correspond to the keys in the {@link InputRequests} map;\nvalues are the client's result for each request.", + "type": "object" + }, + "InternalError": { + "description": "A JSON-RPC error indicating that an internal error occurred on the receiver. This error is returned when the receiver encounters an unexpected condition that prevents it from fulfilling the request.", + "properties": { + "code": { + "const": -32603, + "description": "The error type that occurred.", + "type": "integer" + }, + "data": { + "description": "Additional information about the error. The value of this member is defined by the sender (e.g. detailed error information, nested errors etc.)." + }, + "message": { + "description": "A short description of the error. The message SHOULD be limited to a concise single sentence.", + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + "InvalidParamsError": { + "description": "A JSON-RPC error indicating that the method parameters are invalid or malformed.\n\nIn MCP, this error is returned in various contexts when request parameters fail validation:\n\n- **Tools**: Unknown tool name or invalid tool arguments\n- **Prompts**: Unknown prompt name or missing required arguments\n- **Pagination**: Invalid or expired cursor values\n- **Logging**: Invalid log level\n- **Elicitation**: Server requests an elicitation mode not declared in client capabilities\n- **Sampling**: Missing tool result or tool results mixed with other content", + "properties": { + "code": { + "const": -32602, + "description": "The error type that occurred.", + "type": "integer" + }, + "data": { + "description": "Additional information about the error. The value of this member is defined by the sender (e.g. detailed error information, nested errors etc.)." + }, + "message": { + "description": "A short description of the error. The message SHOULD be limited to a concise single sentence.", + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + "InvalidRequestError": { + "description": "A JSON-RPC error indicating that the request is not a valid request object. This error is returned when the message structure does not conform to the JSON-RPC 2.0 specification requirements for a request (e.g., missing required fields like `jsonrpc` or `method`, or using invalid types for these fields).", + "properties": { + "code": { + "const": -32600, + "description": "The error type that occurred.", + "type": "integer" + }, + "data": { + "description": "Additional information about the error. The value of this member is defined by the sender (e.g. detailed error information, nested errors etc.)." + }, + "message": { + "description": "A short description of the error. The message SHOULD be limited to a concise single sentence.", + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + "JSONArray": { + "items": { + "$ref": "#/$defs/JSONValue" + }, + "type": "array" + }, + "JSONObject": { + "additionalProperties": { + "$ref": "#/$defs/JSONValue" + }, + "type": "object" + }, + "JSONRPCErrorResponse": { + "description": "A response to a request that indicates an error occurred.", + "properties": { + "error": { + "$ref": "#/$defs/Error" + }, + "id": { + "$ref": "#/$defs/RequestId" + }, + "jsonrpc": { + "const": "2.0", + "type": "string" + } + }, + "required": [ + "error", + "jsonrpc" + ], + "type": "object" + }, + "JSONRPCMessage": { + "anyOf": [ + { + "$ref": "#/$defs/JSONRPCRequest" + }, + { + "$ref": "#/$defs/JSONRPCNotification" + }, + { + "$ref": "#/$defs/JSONRPCResultResponse" + }, + { + "$ref": "#/$defs/JSONRPCErrorResponse" + } + ], + "description": "Refers to any valid JSON-RPC object that can be decoded off the wire, or encoded to be sent." + }, + "JSONRPCNotification": { + "description": "A notification which does not expect a response.", + "properties": { + "jsonrpc": { + "const": "2.0", + "type": "string" + }, + "method": { + "type": "string" + }, + "params": { + "additionalProperties": {}, + "type": "object" + } + }, + "required": [ + "jsonrpc", + "method" + ], + "type": "object" + }, + "JSONRPCRequest": { + "description": "A request that expects a response.", + "properties": { + "id": { + "$ref": "#/$defs/RequestId" + }, + "jsonrpc": { + "const": "2.0", + "type": "string" + }, + "method": { + "type": "string" + }, + "params": { + "additionalProperties": {}, + "type": "object" + } + }, + "required": [ + "id", + "jsonrpc", + "method" + ], + "type": "object" + }, + "JSONRPCResponse": { + "anyOf": [ + { + "$ref": "#/$defs/JSONRPCResultResponse" + }, + { + "$ref": "#/$defs/JSONRPCErrorResponse" + } + ], + "description": "A response to a request, containing either the result or error." + }, + "JSONRPCResultResponse": { + "description": "A successful (non-error) response to a request.", + "properties": { + "id": { + "$ref": "#/$defs/RequestId" + }, + "jsonrpc": { + "const": "2.0", + "type": "string" + }, + "result": { + "$ref": "#/$defs/Result" + } + }, + "required": [ + "id", + "jsonrpc", + "result" + ], + "type": "object" + }, + "JSONValue": { + "anyOf": [ + { + "$ref": "#/$defs/JSONObject" + }, + { + "items": { + "$ref": "#/$defs/JSONValue" + }, + "type": "array" + }, + { + "type": [ + "string", + "integer", + "boolean" + ] + } + ] + }, + "LegacyTitledEnumSchema": { + "description": "Use {@link TitledSingleSelectEnumSchema} instead.\nThis interface will be removed in a future version.", + "properties": { + "default": { + "type": "string" + }, + "description": { + "type": "string" + }, + "enum": { + "items": { + "type": "string" + }, + "type": "array" + }, + "enumNames": { + "description": "(Legacy) Display names for enum values.\nNon-standard according to JSON schema 2020-12.", + "items": { + "type": "string" + }, + "type": "array" + }, + "title": { + "type": "string" + }, + "type": { + "const": "string", + "type": "string" + } + }, + "required": [ + "enum", + "type" + ], + "type": "object" + }, + "ListPromptsRequest": { + "description": "Sent from the client to request a list of prompts and prompt templates the server has.", + "properties": { + "id": { + "$ref": "#/$defs/RequestId" + }, + "jsonrpc": { + "const": "2.0", + "type": "string" + }, + "method": { + "const": "prompts/list", + "type": "string" + }, + "params": { + "$ref": "#/$defs/PaginatedRequestParams" + } + }, + "required": [ + "id", + "jsonrpc", + "method", + "params" + ], + "type": "object" + }, + "ListPromptsResult": { + "description": "The result returned by the server for a {@link ListPromptsRequestprompts/list} request.", + "properties": { + "_meta": { + "$ref": "#/$defs/ResultMetaObject" + }, + "cacheScope": { + "description": "Indicates the intended scope of the cached response, analogous to HTTP\n`Cache-Control: public` vs `Cache-Control: private`.\n\n- `\"public\"`: The response does not contain user-specific data. Any\n client or intermediary (e.g., shared gateway, caching proxy) MAY cache\n the response and serve it across authorization contexts.\n- `\"private\"`: The response MAY be cached and reused only within the\n same authorization context. Caches MUST NOT be shared across\n authorization contexts (e.g., a different access token requires a\n different cache).", + "enum": [ + "private", + "public" + ], + "type": "string" + }, + "nextCursor": { + "description": "An opaque token representing the pagination position after the last returned result.\nIf present, there may be more results available.", + "type": "string" + }, + "prompts": { + "items": { + "$ref": "#/$defs/Prompt" + }, + "type": "array" + }, + "resultType": { + "description": "Indicates the type of the result, which allows the client to determine\nhow to parse the result object.\n\nServers implementing this protocol version MUST include this field.\nFor backward compatibility, when a client receives a result from a\nserver implementing an earlier protocol version (which does not include\n`resultType`), the client MUST treat the absent field as `\"complete\"`.", + "type": "string" + }, + "ttlMs": { + "description": "A hint from the server indicating how long (in milliseconds) the\nclient MAY cache this response before re-fetching. Semantics are\nanalogous to HTTP Cache-Control max-age.\n\n- If 0, The response SHOULD be considered immediately stale,\n The client MAY re-fetch every time the result is needed.\n- If positive, the client SHOULD consider the result fresh for this many\n milliseconds after receiving the response.", + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "cacheScope", + "prompts", + "resultType", + "ttlMs" + ], + "type": "object" + }, + "ListPromptsResultResponse": { + "description": "A successful response from the server for a {@link ListPromptsRequestprompts/list} request.", + "properties": { + "id": { + "$ref": "#/$defs/RequestId" + }, + "jsonrpc": { + "const": "2.0", + "type": "string" + }, + "result": { + "$ref": "#/$defs/ListPromptsResult" + } + }, + "required": [ + "id", + "jsonrpc", + "result" + ], + "type": "object" + }, + "ListResourceTemplatesRequest": { + "description": "Sent from the client to request a list of resource templates the server has.", + "properties": { + "id": { + "$ref": "#/$defs/RequestId" + }, + "jsonrpc": { + "const": "2.0", + "type": "string" + }, + "method": { + "const": "resources/templates/list", + "type": "string" + }, + "params": { + "$ref": "#/$defs/PaginatedRequestParams" + } + }, + "required": [ + "id", + "jsonrpc", + "method", + "params" + ], + "type": "object" + }, + "ListResourceTemplatesResult": { + "description": "The result returned by the server for a {@link ListResourceTemplatesRequestresources/templates/list} request.", + "properties": { + "_meta": { + "$ref": "#/$defs/ResultMetaObject" + }, + "cacheScope": { + "description": "Indicates the intended scope of the cached response, analogous to HTTP\n`Cache-Control: public` vs `Cache-Control: private`.\n\n- `\"public\"`: The response does not contain user-specific data. Any\n client or intermediary (e.g., shared gateway, caching proxy) MAY cache\n the response and serve it across authorization contexts.\n- `\"private\"`: The response MAY be cached and reused only within the\n same authorization context. Caches MUST NOT be shared across\n authorization contexts (e.g., a different access token requires a\n different cache).", + "enum": [ + "private", + "public" + ], + "type": "string" + }, + "nextCursor": { + "description": "An opaque token representing the pagination position after the last returned result.\nIf present, there may be more results available.", + "type": "string" + }, + "resourceTemplates": { + "items": { + "$ref": "#/$defs/ResourceTemplate" + }, + "type": "array" + }, + "resultType": { + "description": "Indicates the type of the result, which allows the client to determine\nhow to parse the result object.\n\nServers implementing this protocol version MUST include this field.\nFor backward compatibility, when a client receives a result from a\nserver implementing an earlier protocol version (which does not include\n`resultType`), the client MUST treat the absent field as `\"complete\"`.", + "type": "string" + }, + "ttlMs": { + "description": "A hint from the server indicating how long (in milliseconds) the\nclient MAY cache this response before re-fetching. Semantics are\nanalogous to HTTP Cache-Control max-age.\n\n- If 0, The response SHOULD be considered immediately stale,\n The client MAY re-fetch every time the result is needed.\n- If positive, the client SHOULD consider the result fresh for this many\n milliseconds after receiving the response.", + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "cacheScope", + "resourceTemplates", + "resultType", + "ttlMs" + ], + "type": "object" + }, + "ListResourceTemplatesResultResponse": { + "description": "A successful response from the server for a {@link ListResourceTemplatesRequestresources/templates/list} request.", + "properties": { + "id": { + "$ref": "#/$defs/RequestId" + }, + "jsonrpc": { + "const": "2.0", + "type": "string" + }, + "result": { + "$ref": "#/$defs/ListResourceTemplatesResult" + } + }, + "required": [ + "id", + "jsonrpc", + "result" + ], + "type": "object" + }, + "ListResourcesRequest": { + "description": "Sent from the client to request a list of resources the server has.", + "properties": { + "id": { + "$ref": "#/$defs/RequestId" + }, + "jsonrpc": { + "const": "2.0", + "type": "string" + }, + "method": { + "const": "resources/list", + "type": "string" + }, + "params": { + "$ref": "#/$defs/PaginatedRequestParams" + } + }, + "required": [ + "id", + "jsonrpc", + "method", + "params" + ], + "type": "object" + }, + "ListResourcesResult": { + "description": "The result returned by the server for a {@link ListResourcesRequestresources/list} request.", + "properties": { + "_meta": { + "$ref": "#/$defs/ResultMetaObject" + }, + "cacheScope": { + "description": "Indicates the intended scope of the cached response, analogous to HTTP\n`Cache-Control: public` vs `Cache-Control: private`.\n\n- `\"public\"`: The response does not contain user-specific data. Any\n client or intermediary (e.g., shared gateway, caching proxy) MAY cache\n the response and serve it across authorization contexts.\n- `\"private\"`: The response MAY be cached and reused only within the\n same authorization context. Caches MUST NOT be shared across\n authorization contexts (e.g., a different access token requires a\n different cache).", + "enum": [ + "private", + "public" + ], + "type": "string" + }, + "nextCursor": { + "description": "An opaque token representing the pagination position after the last returned result.\nIf present, there may be more results available.", + "type": "string" + }, + "resources": { + "items": { + "$ref": "#/$defs/Resource" + }, + "type": "array" + }, + "resultType": { + "description": "Indicates the type of the result, which allows the client to determine\nhow to parse the result object.\n\nServers implementing this protocol version MUST include this field.\nFor backward compatibility, when a client receives a result from a\nserver implementing an earlier protocol version (which does not include\n`resultType`), the client MUST treat the absent field as `\"complete\"`.", + "type": "string" + }, + "ttlMs": { + "description": "A hint from the server indicating how long (in milliseconds) the\nclient MAY cache this response before re-fetching. Semantics are\nanalogous to HTTP Cache-Control max-age.\n\n- If 0, The response SHOULD be considered immediately stale,\n The client MAY re-fetch every time the result is needed.\n- If positive, the client SHOULD consider the result fresh for this many\n milliseconds after receiving the response.", + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "cacheScope", + "resources", + "resultType", + "ttlMs" + ], + "type": "object" + }, + "ListResourcesResultResponse": { + "description": "A successful response from the server for a {@link ListResourcesRequestresources/list} request.", + "properties": { + "id": { + "$ref": "#/$defs/RequestId" + }, + "jsonrpc": { + "const": "2.0", + "type": "string" + }, + "result": { + "$ref": "#/$defs/ListResourcesResult" + } + }, + "required": [ + "id", + "jsonrpc", + "result" + ], + "type": "object" + }, + "ListRootsRequest": { + "description": "Sent from the server to request a list of root URIs from the client. Roots allow\nservers to ask for specific directories or files to operate on. A common example\nfor roots is providing a set of repositories or directories a server should operate\non.\n\nThis request is typically used when the server needs to understand the file system\nstructure or access specific locations that the client has permission to read from.", + "properties": { + "method": { + "const": "roots/list", + "type": "string" + }, + "params": { + "properties": { + "_meta": { + "$ref": "#/$defs/MetaObject" + } + }, + "type": "object" + } + }, + "required": [ + "method" + ], + "type": "object" + }, + "ListRootsResult": { + "description": "The result returned by the client for a {@link ListRootsRequestroots/list} request.\nThis result contains an array of {@link Root} objects, each representing a root directory\nor file that the server can operate on.", + "properties": { + "roots": { + "items": { + "$ref": "#/$defs/Root" + }, + "type": "array" + } + }, + "required": [ + "roots" + ], + "type": "object" + }, + "ListToolsRequest": { + "description": "Sent from the client to request a list of tools the server has.", + "properties": { + "id": { + "$ref": "#/$defs/RequestId" + }, + "jsonrpc": { + "const": "2.0", + "type": "string" + }, + "method": { + "const": "tools/list", + "type": "string" + }, + "params": { + "$ref": "#/$defs/PaginatedRequestParams" + } + }, + "required": [ + "id", + "jsonrpc", + "method", + "params" + ], + "type": "object" + }, + "ListToolsResult": { + "description": "The result returned by the server for a {@link ListToolsRequesttools/list} request.", + "properties": { + "_meta": { + "$ref": "#/$defs/ResultMetaObject" + }, + "cacheScope": { + "description": "Indicates the intended scope of the cached response, analogous to HTTP\n`Cache-Control: public` vs `Cache-Control: private`.\n\n- `\"public\"`: The response does not contain user-specific data. Any\n client or intermediary (e.g., shared gateway, caching proxy) MAY cache\n the response and serve it across authorization contexts.\n- `\"private\"`: The response MAY be cached and reused only within the\n same authorization context. Caches MUST NOT be shared across\n authorization contexts (e.g., a different access token requires a\n different cache).", + "enum": [ + "private", + "public" + ], + "type": "string" + }, + "nextCursor": { + "description": "An opaque token representing the pagination position after the last returned result.\nIf present, there may be more results available.", + "type": "string" + }, + "resultType": { + "description": "Indicates the type of the result, which allows the client to determine\nhow to parse the result object.\n\nServers implementing this protocol version MUST include this field.\nFor backward compatibility, when a client receives a result from a\nserver implementing an earlier protocol version (which does not include\n`resultType`), the client MUST treat the absent field as `\"complete\"`.", + "type": "string" + }, + "tools": { + "items": { + "$ref": "#/$defs/Tool" + }, + "type": "array" + }, + "ttlMs": { + "description": "A hint from the server indicating how long (in milliseconds) the\nclient MAY cache this response before re-fetching. Semantics are\nanalogous to HTTP Cache-Control max-age.\n\n- If 0, The response SHOULD be considered immediately stale,\n The client MAY re-fetch every time the result is needed.\n- If positive, the client SHOULD consider the result fresh for this many\n milliseconds after receiving the response.", + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "cacheScope", + "resultType", + "tools", + "ttlMs" + ], + "type": "object" + }, + "ListToolsResultResponse": { + "description": "A successful response from the server for a {@link ListToolsRequesttools/list} request.", + "properties": { + "id": { + "$ref": "#/$defs/RequestId" + }, + "jsonrpc": { + "const": "2.0", + "type": "string" + }, + "result": { + "$ref": "#/$defs/ListToolsResult" + } + }, + "required": [ + "id", + "jsonrpc", + "result" + ], + "type": "object" + }, + "LoggingLevel": { + "description": "The severity of a log message.\n\nThese map to syslog message severities, as specified in RFC-5424:\nhttps://datatracker.ietf.org/doc/html/rfc5424#section-6.2.1", + "enum": [ + "alert", + "critical", + "debug", + "emergency", + "error", + "info", + "notice", + "warning" + ], + "type": "string" + }, + "LoggingMessageNotification": { + "description": "JSONRPCNotification of a log message passed from server to client. The client opts in by setting `\"io.modelcontextprotocol/logLevel\"` in a request's `_meta`.", + "properties": { + "jsonrpc": { + "const": "2.0", + "type": "string" + }, + "method": { + "const": "notifications/message", + "type": "string" + }, + "params": { + "$ref": "#/$defs/LoggingMessageNotificationParams" + } + }, + "required": [ + "jsonrpc", + "method", + "params" + ], + "type": "object" + }, + "LoggingMessageNotificationParams": { + "description": "Parameters for a `notifications/message` notification.", + "properties": { + "_meta": { + "$ref": "#/$defs/NotificationMetaObject" + }, + "data": { + "description": "The data to be logged, such as a string message or an object. Any JSON serializable type is allowed here." + }, + "level": { + "$ref": "#/$defs/LoggingLevel", + "description": "The severity of this log message." + }, + "logger": { + "description": "An optional name of the logger issuing this message.", + "type": "string" + } + }, + "required": [ + "data", + "level" + ], + "type": "object" + }, + "MetaObject": { + "description": "Represents the contents of a `_meta` field, which clients and servers use to attach additional metadata to their interactions.\n\nCertain key names are reserved by MCP for protocol-level metadata; implementations MUST NOT make assumptions about values at these keys. Additionally, specific schema definitions may reserve particular names for purpose-specific metadata, as declared in those definitions.\n\nValid keys have two segments:\n\n**Prefix:**\n- Optional — if specified, MUST be a series of _labels_ separated by dots (`.`), followed by a slash (`/`).\n- Labels MUST start with a letter and end with a letter or digit. Interior characters may be letters, digits, or hyphens (`-`).\n- Implementations SHOULD use reverse DNS notation (e.g., `com.example/` rather than `example.com/`).\n- Any prefix where the second label is `modelcontextprotocol` or `mcp` is **reserved** for MCP use. For example: `io.modelcontextprotocol/`, `dev.mcp/`, `org.modelcontextprotocol.api/`, and `com.mcp.tools/` are all reserved. However, `com.example.mcp/` is NOT reserved, as the second label is `example`.\n\n**Name:**\n- Unless empty, MUST start and end with an alphanumeric character (`[a-z0-9A-Z]`).\n- Interior characters may be alphanumeric, hyphens (`-`), underscores (`_`), or dots (`.`).", + "type": "object" + }, + "MethodNotFoundError": { + "description": "A JSON-RPC error indicating that the requested method does not exist or is not available.\n\nIn MCP, a server returns this error when a client invokes a method the server does not implement — either a genuinely unknown method, or one gated behind a server capability the server did not advertise (e.g., calling `prompts/list` when the `prompts` capability was not advertised).\n\nA request that requires a client capability the client did not declare is signalled instead by {@link MissingRequiredClientCapabilityError} (`-32021`).", + "properties": { + "code": { + "const": -32601, + "description": "The error type that occurred.", + "type": "integer" + }, + "data": { + "description": "Additional information about the error. The value of this member is defined by the sender (e.g. detailed error information, nested errors etc.)." + }, + "message": { + "description": "A short description of the error. The message SHOULD be limited to a concise single sentence.", + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + "MissingRequiredClientCapabilityError": { + "description": "Returned when processing a request requires a capability the client did not\ndeclare in `clientCapabilities`. For HTTP, the response status code MUST be\n`400 Bad Request`.", + "properties": { + "error": { + "allOf": [ + { + "$ref": "#/$defs/Error" + }, + { + "properties": { + "code": { + "const": -32021, + "type": "integer" + }, + "data": { + "properties": { + "requiredCapabilities": { + "$ref": "#/$defs/ClientCapabilities", + "description": "The capabilities the server requires from the client to process this request." + } + }, + "required": [ + "requiredCapabilities" + ], + "type": "object" + } + }, + "required": [ + "code", + "data" + ], + "type": "object" + } + ] + }, + "id": { + "$ref": "#/$defs/RequestId" + }, + "jsonrpc": { + "const": "2.0", + "type": "string" + } + }, + "required": [ + "error", + "jsonrpc" + ], + "type": "object" + }, + "ModelHint": { + "description": "Hints to use for model selection.\n\nKeys not declared here are currently left unspecified by the spec and are up\nto the client to interpret.", + "properties": { + "name": { + "description": "A hint for a model name.\n\nThe client SHOULD treat this as a substring of a model name; for example:\n - `claude-3-5-sonnet` should match `claude-3-5-sonnet-20241022`\n - `sonnet` should match `claude-3-5-sonnet-20241022`, `claude-3-sonnet-20240229`, etc.\n - `claude` should match any Claude model\n\nThe client MAY also map the string to a different provider's model name or a different model family, as long as it fills a similar niche; for example:\n - `gemini-1.5-flash` could match `claude-3-haiku-20240307`", + "type": "string" + } + }, + "type": "object" + }, + "ModelPreferences": { + "description": "The server's preferences for model selection, requested of the client during sampling.\n\nBecause LLMs can vary along multiple dimensions, choosing the \"best\" model is\nrarely straightforward. Different models excel in different areas—some are\nfaster but less capable, others are more capable but more expensive, and so\non. This interface allows servers to express their priorities across multiple\ndimensions to help clients make an appropriate selection for their use case.\n\nThese preferences are always advisory. The client MAY ignore them. It is also\nup to the client to decide how to interpret these preferences and how to\nbalance them against other considerations.", + "properties": { + "costPriority": { + "description": "How much to prioritize cost when selecting a model. A value of 0 means cost\nis not important, while a value of 1 means cost is the most important\nfactor.", + "maximum": 1, + "minimum": 0, + "type": "number" + }, + "hints": { + "description": "Optional hints to use for model selection.\n\nIf multiple hints are specified, the client MUST evaluate them in order\n(such that the first match is taken).\n\nThe client SHOULD prioritize these hints over the numeric priorities, but\nMAY still use the priorities to select from ambiguous matches.", + "items": { + "$ref": "#/$defs/ModelHint" + }, + "type": "array" + }, + "intelligencePriority": { + "description": "How much to prioritize intelligence and capabilities when selecting a\nmodel. A value of 0 means intelligence is not important, while a value of 1\nmeans intelligence is the most important factor.", + "maximum": 1, + "minimum": 0, + "type": "number" + }, + "speedPriority": { + "description": "How much to prioritize sampling speed (latency) when selecting a model. A\nvalue of 0 means speed is not important, while a value of 1 means speed is\nthe most important factor.", + "maximum": 1, + "minimum": 0, + "type": "number" + } + }, + "type": "object" + }, + "MultiSelectEnumSchema": { + "anyOf": [ + { + "$ref": "#/$defs/UntitledMultiSelectEnumSchema" + }, + { + "$ref": "#/$defs/TitledMultiSelectEnumSchema" + } + ] + }, + "Notification": { + "properties": { + "method": { + "type": "string" + }, + "params": { + "additionalProperties": {}, + "type": "object" + } + }, + "required": [ + "method" + ], + "type": "object" + }, + "NotificationMetaObject": { + "description": "Extends {@link MetaObject} with additional notification-specific fields. All key naming rules from `MetaObject` apply.", + "properties": { + "io.modelcontextprotocol/subscriptionId": { + "$ref": "#/$defs/RequestId", + "description": "Identifies the subscription stream a notification was delivered on. The\nserver MUST include this key on every notification delivered via a\n{@link SubscriptionsListenRequestsubscriptions/listen} stream, so the\nclient can correlate the notification with the originating subscription.\nThe key is absent on notifications not delivered via a subscription\nstream (e.g. progress notifications for an in-flight request), which is\nwhy it is optional here.\n\nThe value is the JSON-RPC ID of the `subscriptions/listen` request that\nopened the stream." + } + }, + "type": "object" + }, + "NotificationParams": { + "description": "Common params for any notification.", + "properties": { + "_meta": { + "$ref": "#/$defs/NotificationMetaObject" + } + }, + "type": "object" + }, + "NumberSchema": { + "properties": { + "default": { + "type": "number" + }, + "description": { + "type": "string" + }, + "maximum": { + "type": "number" + }, + "minimum": { + "type": "number" + }, + "title": { + "type": "string" + }, + "type": { + "enum": [ + "integer", + "number" + ], + "type": "string" + } + }, + "required": [ + "type" + ], + "type": "object" + }, + "PaginatedRequest": { + "properties": { + "id": { + "$ref": "#/$defs/RequestId" + }, + "jsonrpc": { + "const": "2.0", + "type": "string" + }, + "method": { + "type": "string" + }, + "params": { + "$ref": "#/$defs/PaginatedRequestParams" + } + }, + "required": [ + "id", + "jsonrpc", + "method", + "params" + ], + "type": "object" + }, + "PaginatedRequestParams": { + "description": "Common params for paginated requests.", + "properties": { + "_meta": { + "$ref": "#/$defs/RequestMetaObject" + }, + "cursor": { + "description": "An opaque token representing the current pagination position.\nIf provided, the server should return results starting after this cursor.", + "type": "string" + } + }, + "required": [ + "_meta" + ], + "type": "object" + }, + "PaginatedResult": { + "properties": { + "_meta": { + "$ref": "#/$defs/ResultMetaObject" + }, + "nextCursor": { + "description": "An opaque token representing the pagination position after the last returned result.\nIf present, there may be more results available.", + "type": "string" + }, + "resultType": { + "description": "Indicates the type of the result, which allows the client to determine\nhow to parse the result object.\n\nServers implementing this protocol version MUST include this field.\nFor backward compatibility, when a client receives a result from a\nserver implementing an earlier protocol version (which does not include\n`resultType`), the client MUST treat the absent field as `\"complete\"`.", + "type": "string" + } + }, + "required": [ + "resultType" + ], + "type": "object" + }, + "ParseError": { + "description": "A JSON-RPC error indicating that invalid JSON was received by the server. This error is returned when the server cannot parse the JSON text of a message.", + "properties": { + "code": { + "const": -32700, + "description": "The error type that occurred.", + "type": "integer" + }, + "data": { + "description": "Additional information about the error. The value of this member is defined by the sender (e.g. detailed error information, nested errors etc.)." + }, + "message": { + "description": "A short description of the error. The message SHOULD be limited to a concise single sentence.", + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + "PrimitiveSchemaDefinition": { + "anyOf": [ + { + "$ref": "#/$defs/StringSchema" + }, + { + "$ref": "#/$defs/NumberSchema" + }, + { + "$ref": "#/$defs/BooleanSchema" + }, + { + "$ref": "#/$defs/UntitledSingleSelectEnumSchema" + }, + { + "$ref": "#/$defs/TitledSingleSelectEnumSchema" + }, + { + "$ref": "#/$defs/UntitledMultiSelectEnumSchema" + }, + { + "$ref": "#/$defs/TitledMultiSelectEnumSchema" + }, + { + "$ref": "#/$defs/LegacyTitledEnumSchema" + } + ], + "description": "Restricted schema definitions that only allow primitive types\nwithout nested objects or arrays." + }, + "ProgressNotification": { + "description": "An out-of-band notification used to inform the receiver of a progress update for a long-running request.", + "properties": { + "jsonrpc": { + "const": "2.0", + "type": "string" + }, + "method": { + "const": "notifications/progress", + "type": "string" + }, + "params": { + "$ref": "#/$defs/ProgressNotificationParams" + } + }, + "required": [ + "jsonrpc", + "method", + "params" + ], + "type": "object" + }, + "ProgressNotificationParams": { + "description": "Parameters for a {@link ProgressNotificationnotifications/progress} notification.", + "properties": { + "_meta": { + "$ref": "#/$defs/NotificationMetaObject" + }, + "message": { + "description": "An optional message describing the current progress.", + "type": "string" + }, + "progress": { + "description": "The progress thus far. This should increase every time progress is made, even if the total is unknown.", + "type": "number" + }, + "progressToken": { + "$ref": "#/$defs/ProgressToken", + "description": "The progress token which was given in the initial request, used to associate this notification with the request that is proceeding." + }, + "total": { + "description": "Total number of items to process (or total progress required), if known.", + "type": "number" + } + }, + "required": [ + "progress", + "progressToken" + ], + "type": "object" + }, + "ProgressToken": { + "description": "A progress token, used to associate progress notifications with the original request.", + "type": [ + "string", + "integer" + ] + }, + "Prompt": { + "description": "A prompt or prompt template that the server offers.", + "properties": { + "_meta": { + "$ref": "#/$defs/MetaObject" + }, + "arguments": { + "description": "A list of arguments to use for templating the prompt.", + "items": { + "$ref": "#/$defs/PromptArgument" + }, + "type": "array" + }, + "description": { + "description": "An optional description of what this prompt provides", + "type": "string" + }, + "icons": { + "description": "Optional set of sized icons that the client can display in a user interface.\n\nClients that support rendering icons MUST support at least the following MIME types:\n- `image/png` - PNG images (safe, universal compatibility)\n- `image/jpeg` (and `image/jpg`) - JPEG images (safe, universal compatibility)\n\nClients that support rendering icons SHOULD also support:\n- `image/svg+xml` - SVG images (scalable but requires security precautions)\n- `image/webp` - WebP images (modern, efficient format)", + "items": { + "$ref": "#/$defs/Icon" + }, + "type": "array" + }, + "name": { + "description": "Intended for programmatic or logical use, but used as a display name in past specs or fallback (if title isn't present).", + "type": "string" + }, + "title": { + "description": "Intended for UI and end-user contexts — optimized to be human-readable and easily understood,\neven by those unfamiliar with domain-specific terminology.\n\nIf not provided, the name should be used for display (except for {@link Tool},\nwhere `annotations.title` should be given precedence over using `name`,\nif present).", + "type": "string" + } + }, + "required": [ + "name" + ], + "type": "object" + }, + "PromptArgument": { + "description": "Describes an argument that a prompt can accept.", + "properties": { + "description": { + "description": "A human-readable description of the argument.", + "type": "string" + }, + "name": { + "description": "Intended for programmatic or logical use, but used as a display name in past specs or fallback (if title isn't present).", + "type": "string" + }, + "required": { + "description": "Whether this argument must be provided.", + "type": "boolean" + }, + "title": { + "description": "Intended for UI and end-user contexts — optimized to be human-readable and easily understood,\neven by those unfamiliar with domain-specific terminology.\n\nIf not provided, the name should be used for display (except for {@link Tool},\nwhere `annotations.title` should be given precedence over using `name`,\nif present).", + "type": "string" + } + }, + "required": [ + "name" + ], + "type": "object" + }, + "PromptListChangedNotification": { + "description": "An optional notification from the server to the client, informing it that the list of prompts it offers has changed. This is only delivered on a {@link SubscriptionsListenRequestsubscriptions/listen} stream when the client requested it via the `promptsListChanged` filter field.", + "properties": { + "jsonrpc": { + "const": "2.0", + "type": "string" + }, + "method": { + "const": "notifications/prompts/list_changed", + "type": "string" + }, + "params": { + "$ref": "#/$defs/NotificationParams" + } + }, + "required": [ + "jsonrpc", + "method" + ], + "type": "object" + }, + "PromptMessage": { + "description": "Describes a message returned as part of a prompt.\n\nThis is similar to {@link SamplingMessage}, but also supports the embedding of\nresources from the MCP server.", + "properties": { + "content": { + "$ref": "#/$defs/ContentBlock" + }, + "role": { + "$ref": "#/$defs/Role" + } + }, + "required": [ + "content", + "role" + ], + "type": "object" + }, + "PromptReference": { + "description": "Identifies a prompt.", + "properties": { + "name": { + "description": "Intended for programmatic or logical use, but used as a display name in past specs or fallback (if title isn't present).", + "type": "string" + }, + "title": { + "description": "Intended for UI and end-user contexts — optimized to be human-readable and easily understood,\neven by those unfamiliar with domain-specific terminology.\n\nIf not provided, the name should be used for display (except for {@link Tool},\nwhere `annotations.title` should be given precedence over using `name`,\nif present).", + "type": "string" + }, + "type": { + "const": "ref/prompt", + "type": "string" + } + }, + "required": [ + "name", + "type" + ], + "type": "object" + }, + "ReadResourceRequest": { + "description": "Sent from the client to the server, to read a specific resource URI.", + "properties": { + "id": { + "$ref": "#/$defs/RequestId" + }, + "jsonrpc": { + "const": "2.0", + "type": "string" + }, + "method": { + "const": "resources/read", + "type": "string" + }, + "params": { + "$ref": "#/$defs/ReadResourceRequestParams" + } + }, + "required": [ + "id", + "jsonrpc", + "method", + "params" + ], + "type": "object" + }, + "ReadResourceRequestParams": { + "description": "Parameters for a `resources/read` request.", + "properties": { + "_meta": { + "$ref": "#/$defs/RequestMetaObject" + }, + "inputResponses": { + "$ref": "#/$defs/InputResponses" + }, + "requestState": { + "type": "string" + }, + "uri": { + "description": "The URI of the resource. The URI can use any protocol; it is up to the server how to interpret it.", + "format": "uri", + "type": "string" + } + }, + "required": [ + "_meta", + "uri" + ], + "type": "object" + }, + "ReadResourceResult": { + "description": "The result returned by the server for a {@link ReadResourceRequestresources/read} request.", + "properties": { + "_meta": { + "$ref": "#/$defs/ResultMetaObject" + }, + "cacheScope": { + "description": "Indicates the intended scope of the cached response, analogous to HTTP\n`Cache-Control: public` vs `Cache-Control: private`.\n\n- `\"public\"`: The response does not contain user-specific data. Any\n client or intermediary (e.g., shared gateway, caching proxy) MAY cache\n the response and serve it across authorization contexts.\n- `\"private\"`: The response MAY be cached and reused only within the\n same authorization context. Caches MUST NOT be shared across\n authorization contexts (e.g., a different access token requires a\n different cache).", + "enum": [ + "private", + "public" + ], + "type": "string" + }, + "contents": { + "items": { + "anyOf": [ + { + "$ref": "#/$defs/TextResourceContents" + }, + { + "$ref": "#/$defs/BlobResourceContents" + } + ] + }, + "type": "array" + }, + "resultType": { + "description": "Indicates the type of the result, which allows the client to determine\nhow to parse the result object.\n\nServers implementing this protocol version MUST include this field.\nFor backward compatibility, when a client receives a result from a\nserver implementing an earlier protocol version (which does not include\n`resultType`), the client MUST treat the absent field as `\"complete\"`.", + "type": "string" + }, + "ttlMs": { + "description": "A hint from the server indicating how long (in milliseconds) the\nclient MAY cache this response before re-fetching. Semantics are\nanalogous to HTTP Cache-Control max-age.\n\n- If 0, The response SHOULD be considered immediately stale,\n The client MAY re-fetch every time the result is needed.\n- If positive, the client SHOULD consider the result fresh for this many\n milliseconds after receiving the response.", + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "cacheScope", + "contents", + "resultType", + "ttlMs" + ], + "type": "object" + }, + "ReadResourceResultResponse": { + "description": "A successful response from the server for a {@link ReadResourceRequestresources/read} request.", + "properties": { + "id": { + "$ref": "#/$defs/RequestId" + }, + "jsonrpc": { + "const": "2.0", + "type": "string" + }, + "result": { + "anyOf": [ + { + "$ref": "#/$defs/InputRequiredResult" + }, + { + "$ref": "#/$defs/ReadResourceResult" + } + ] + } + }, + "required": [ + "id", + "jsonrpc", + "result" + ], + "type": "object" + }, + "Request": { + "properties": { + "method": { + "type": "string" + }, + "params": { + "additionalProperties": {}, + "type": "object" + } + }, + "required": [ + "method" + ], + "type": "object" + }, + "RequestId": { + "description": "A uniquely identifying ID for a request in JSON-RPC.", + "type": [ + "string", + "integer" + ] + }, + "RequestMetaObject": { + "description": "Extends {@link MetaObject} with additional request-specific fields. All key naming rules from `MetaObject` apply.", + "properties": { + "io.modelcontextprotocol/clientCapabilities": { + "$ref": "#/$defs/ClientCapabilities", + "description": "The client's capabilities for this specific request. Required.\n\nCapabilities are declared per-request rather than once at initialization;\nan empty object means the client supports no optional capabilities.\nServers MUST NOT infer capabilities from prior requests." + }, + "io.modelcontextprotocol/clientInfo": { + "$ref": "#/$defs/Implementation", + "description": "Identifies the client software making the request. Clients SHOULD\ninclude this field on every request unless specifically configured not\nto do so.\n\nThe {@link Implementation} schema requires `name` and `version`; other\nfields are optional.\n\nThe value is self-reported by the client and is not verified by the\nprotocol. It is intended for display, logging, and debugging. Servers\nSHOULD NOT use it to change their behavior, and SHOULD NOT rely on it for\nsecurity decisions." + }, + "io.modelcontextprotocol/logLevel": { + "$ref": "#/$defs/LoggingLevel", + "description": "The desired log level for this request. Optional.\n\nIf absent, the server MUST NOT send any {@link LoggingMessageNotificationnotifications/message}\nnotifications for this request. The client opts in to log messages by\nexplicitly setting a level. Replaces the former `logging/setLevel` RPC." + }, + "io.modelcontextprotocol/protocolVersion": { + "description": "The MCP Protocol Version being used for this request. Required.\n\nFor the HTTP transport, this value MUST match the `MCP-Protocol-Version`\nheader; otherwise the server MUST return a `400 Bad Request`. If the\nserver does not support the requested version, it MUST return an\n{@link UnsupportedProtocolVersionError}.", + "type": "string" + }, + "progressToken": { + "$ref": "#/$defs/ProgressToken", + "description": "If specified, the caller is requesting out-of-band progress notifications for this request (as represented by {@link ProgressNotificationnotifications/progress}). The value of this parameter is an opaque token that will be attached to any subsequent notifications. The receiver is not obligated to provide these notifications." + } + }, + "required": [ + "io.modelcontextprotocol/clientCapabilities", + "io.modelcontextprotocol/protocolVersion" + ], + "type": "object" + }, + "RequestParams": { + "description": "Common params for any request.", + "properties": { + "_meta": { + "$ref": "#/$defs/RequestMetaObject" + } + }, + "required": [ + "_meta" + ], + "type": "object" + }, + "Resource": { + "description": "A known resource that the server is capable of reading.", + "properties": { + "_meta": { + "$ref": "#/$defs/MetaObject" + }, + "annotations": { + "$ref": "#/$defs/Annotations", + "description": "Optional annotations for the client." + }, + "description": { + "description": "A description of what this resource represents.\n\nThis can be used by clients to improve the LLM's understanding of available resources. It can be thought of like a \"hint\" to the model.", + "type": "string" + }, + "icons": { + "description": "Optional set of sized icons that the client can display in a user interface.\n\nClients that support rendering icons MUST support at least the following MIME types:\n- `image/png` - PNG images (safe, universal compatibility)\n- `image/jpeg` (and `image/jpg`) - JPEG images (safe, universal compatibility)\n\nClients that support rendering icons SHOULD also support:\n- `image/svg+xml` - SVG images (scalable but requires security precautions)\n- `image/webp` - WebP images (modern, efficient format)", + "items": { + "$ref": "#/$defs/Icon" + }, + "type": "array" + }, + "mimeType": { + "description": "The MIME type of this resource, if known.", + "type": "string" + }, + "name": { + "description": "Intended for programmatic or logical use, but used as a display name in past specs or fallback (if title isn't present).", + "type": "string" + }, + "size": { + "description": "The size of the raw resource content, in bytes (i.e., before base64 encoding or any tokenization), if known.\n\nThis can be used by Hosts to display file sizes and estimate context window usage.", + "type": "integer" + }, + "title": { + "description": "Intended for UI and end-user contexts — optimized to be human-readable and easily understood,\neven by those unfamiliar with domain-specific terminology.\n\nIf not provided, the name should be used for display (except for {@link Tool},\nwhere `annotations.title` should be given precedence over using `name`,\nif present).", + "type": "string" + }, + "uri": { + "description": "The URI of this resource.", + "format": "uri", + "type": "string" + } + }, + "required": [ + "name", + "uri" + ], + "type": "object" + }, + "ResourceContents": { + "description": "The contents of a specific resource or sub-resource.", + "properties": { + "_meta": { + "$ref": "#/$defs/MetaObject" + }, + "mimeType": { + "description": "The MIME type of this resource, if known.", + "type": "string" + }, + "uri": { + "description": "The URI of this resource.", + "format": "uri", + "type": "string" + } + }, + "required": [ + "uri" + ], + "type": "object" + }, + "ResourceLink": { + "description": "A resource that the server is capable of reading, included in a prompt or tool call result.\n\nNote: resource links returned by tools are not guaranteed to appear in the results of {@link ListResourcesRequestresources/list} requests.", + "properties": { + "_meta": { + "$ref": "#/$defs/MetaObject" + }, + "annotations": { + "$ref": "#/$defs/Annotations", + "description": "Optional annotations for the client." + }, + "description": { + "description": "A description of what this resource represents.\n\nThis can be used by clients to improve the LLM's understanding of available resources. It can be thought of like a \"hint\" to the model.", + "type": "string" + }, + "icons": { + "description": "Optional set of sized icons that the client can display in a user interface.\n\nClients that support rendering icons MUST support at least the following MIME types:\n- `image/png` - PNG images (safe, universal compatibility)\n- `image/jpeg` (and `image/jpg`) - JPEG images (safe, universal compatibility)\n\nClients that support rendering icons SHOULD also support:\n- `image/svg+xml` - SVG images (scalable but requires security precautions)\n- `image/webp` - WebP images (modern, efficient format)", + "items": { + "$ref": "#/$defs/Icon" + }, + "type": "array" + }, + "mimeType": { + "description": "The MIME type of this resource, if known.", + "type": "string" + }, + "name": { + "description": "Intended for programmatic or logical use, but used as a display name in past specs or fallback (if title isn't present).", + "type": "string" + }, + "size": { + "description": "The size of the raw resource content, in bytes (i.e., before base64 encoding or any tokenization), if known.\n\nThis can be used by Hosts to display file sizes and estimate context window usage.", + "type": "integer" + }, + "title": { + "description": "Intended for UI and end-user contexts — optimized to be human-readable and easily understood,\neven by those unfamiliar with domain-specific terminology.\n\nIf not provided, the name should be used for display (except for {@link Tool},\nwhere `annotations.title` should be given precedence over using `name`,\nif present).", + "type": "string" + }, + "type": { + "const": "resource_link", + "type": "string" + }, + "uri": { + "description": "The URI of this resource.", + "format": "uri", + "type": "string" + } + }, + "required": [ + "name", + "type", + "uri" + ], + "type": "object" + }, + "ResourceListChangedNotification": { + "description": "An optional notification from the server to the client, informing it that the list of resources it can read from has changed. This is only delivered on a {@link SubscriptionsListenRequestsubscriptions/listen} stream when the client requested it via the `resourcesListChanged` filter field.", + "properties": { + "jsonrpc": { + "const": "2.0", + "type": "string" + }, + "method": { + "const": "notifications/resources/list_changed", + "type": "string" + }, + "params": { + "$ref": "#/$defs/NotificationParams" + } + }, + "required": [ + "jsonrpc", + "method" + ], + "type": "object" + }, + "ResourceRequestParams": { + "description": "Common params for resource-related requests.", + "properties": { + "_meta": { + "$ref": "#/$defs/RequestMetaObject" + }, + "uri": { + "description": "The URI of the resource. The URI can use any protocol; it is up to the server how to interpret it.", + "format": "uri", + "type": "string" + } + }, + "required": [ + "_meta", + "uri" + ], + "type": "object" + }, + "ResourceTemplate": { + "description": "A template description for resources available on the server.", + "properties": { + "_meta": { + "$ref": "#/$defs/MetaObject" + }, + "annotations": { + "$ref": "#/$defs/Annotations", + "description": "Optional annotations for the client." + }, + "description": { + "description": "A description of what this template is for.\n\nThis can be used by clients to improve the LLM's understanding of available resources. It can be thought of like a \"hint\" to the model.", + "type": "string" + }, + "icons": { + "description": "Optional set of sized icons that the client can display in a user interface.\n\nClients that support rendering icons MUST support at least the following MIME types:\n- `image/png` - PNG images (safe, universal compatibility)\n- `image/jpeg` (and `image/jpg`) - JPEG images (safe, universal compatibility)\n\nClients that support rendering icons SHOULD also support:\n- `image/svg+xml` - SVG images (scalable but requires security precautions)\n- `image/webp` - WebP images (modern, efficient format)", + "items": { + "$ref": "#/$defs/Icon" + }, + "type": "array" + }, + "mimeType": { + "description": "The MIME type for all resources that match this template. This should only be included if all resources matching this template have the same type.", + "type": "string" + }, + "name": { + "description": "Intended for programmatic or logical use, but used as a display name in past specs or fallback (if title isn't present).", + "type": "string" + }, + "title": { + "description": "Intended for UI and end-user contexts — optimized to be human-readable and easily understood,\neven by those unfamiliar with domain-specific terminology.\n\nIf not provided, the name should be used for display (except for {@link Tool},\nwhere `annotations.title` should be given precedence over using `name`,\nif present).", + "type": "string" + }, + "uriTemplate": { + "description": "A URI template (according to RFC 6570) that can be used to construct resource URIs.", + "format": "uri-template", + "type": "string" + } + }, + "required": [ + "name", + "uriTemplate" + ], + "type": "object" + }, + "ResourceTemplateReference": { + "description": "A reference to a resource or resource template definition.", + "properties": { + "type": { + "const": "ref/resource", + "type": "string" + }, + "uri": { + "description": "The URI or URI template of the resource.", + "format": "uri-template", + "type": "string" + } + }, + "required": [ + "type", + "uri" + ], + "type": "object" + }, + "ResourceUpdatedNotification": { + "description": "A notification from the server to the client, informing it that a resource has changed and may need to be read again. This is only sent for resources the client opted in to via the `resourceSubscriptions` field of a {@link SubscriptionsListenRequestsubscriptions/listen} request.", + "properties": { + "jsonrpc": { + "const": "2.0", + "type": "string" + }, + "method": { + "const": "notifications/resources/updated", + "type": "string" + }, + "params": { + "$ref": "#/$defs/ResourceUpdatedNotificationParams" + } + }, + "required": [ + "jsonrpc", + "method", + "params" + ], + "type": "object" + }, + "ResourceUpdatedNotificationParams": { + "description": "Parameters for a `notifications/resources/updated` notification.", + "properties": { + "_meta": { + "$ref": "#/$defs/NotificationMetaObject" + }, + "uri": { + "description": "The URI of the resource that has been updated. This might be a sub-resource of the one that the client actually subscribed to.", + "format": "uri", + "type": "string" + } + }, + "required": [ + "uri" + ], + "type": "object" + }, + "Result": { + "additionalProperties": {}, + "description": "Common result fields.", + "properties": { + "_meta": { + "$ref": "#/$defs/ResultMetaObject" + }, + "resultType": { + "description": "Indicates the type of the result, which allows the client to determine\nhow to parse the result object.\n\nServers implementing this protocol version MUST include this field.\nFor backward compatibility, when a client receives a result from a\nserver implementing an earlier protocol version (which does not include\n`resultType`), the client MUST treat the absent field as `\"complete\"`.", + "type": "string" + } + }, + "required": [ + "resultType" + ], + "type": "object" + }, + "ResultMetaObject": { + "description": "Extends {@link MetaObject} with additional result-specific fields. All key naming rules from `MetaObject` apply.", + "properties": { + "io.modelcontextprotocol/serverInfo": { + "$ref": "#/$defs/Implementation", + "description": "Identifies the server software producing the response. Servers SHOULD\ninclude this field on every response unless specifically configured not\nto do so.\n\nThe {@link Implementation} schema requires `name` and `version`; other\nfields are optional.\n\nThe value is self-reported by the server and is not verified by the\nprotocol. It is intended for display, logging, and debugging. Clients\nSHOULD NOT use it to change their behavior, and SHOULD NOT rely on it for\nsecurity decisions." + } + }, + "type": "object" + }, + "ResultType": { + "description": "Indicates the type of a {@link Result} object, allowing the client to\ndetermine how to parse the response.\n\ncomplete - the request completed successfully and the result contains the final content.\ninput_required - the request requires additional input and the result contains an {@link InputRequiredResult} object with instructions for the client to provide additional input before retrying the original request.", + "type": "string" + }, + "Role": { + "description": "The sender or recipient of messages and data in a conversation.", + "enum": [ + "assistant", + "user" + ], + "type": "string" + }, + "Root": { + "description": "Represents a root directory or file that the server can operate on.", + "properties": { + "_meta": { + "$ref": "#/$defs/MetaObject" + }, + "name": { + "description": "An optional name for the root. This can be used to provide a human-readable\nidentifier for the root, which may be useful for display purposes or for\nreferencing the root in other parts of the application.", + "type": "string" + }, + "uri": { + "description": "The URI identifying the root. This *must* start with `file://` for now.\nThis restriction may be relaxed in future versions of the protocol to allow\nother URI schemes.", + "format": "uri", + "type": "string" + } + }, + "required": [ + "uri" + ], + "type": "object" + }, + "SamplingMessage": { + "description": "Describes a message issued to or received from an LLM API.", + "properties": { + "_meta": { + "$ref": "#/$defs/MetaObject" + }, + "content": { + "anyOf": [ + { + "$ref": "#/$defs/TextContent" + }, + { + "$ref": "#/$defs/ImageContent" + }, + { + "$ref": "#/$defs/AudioContent" + }, + { + "$ref": "#/$defs/ToolUseContent" + }, + { + "$ref": "#/$defs/ToolResultContent" + }, + { + "items": { + "$ref": "#/$defs/SamplingMessageContentBlock" + }, + "type": "array" + } + ] + }, + "role": { + "$ref": "#/$defs/Role" + } + }, + "required": [ + "content", + "role" + ], + "type": "object" + }, + "SamplingMessageContentBlock": { + "anyOf": [ + { + "$ref": "#/$defs/TextContent" + }, + { + "$ref": "#/$defs/ImageContent" + }, + { + "$ref": "#/$defs/AudioContent" + }, + { + "$ref": "#/$defs/ToolUseContent" + }, + { + "$ref": "#/$defs/ToolResultContent" + } + ] + }, + "ServerCapabilities": { + "description": "Capabilities that a server may support. Known capabilities are defined here, in this schema, but this is not a closed set: any server can define its own, additional capabilities.", + "properties": { + "completions": { + "$ref": "#/$defs/JSONObject", + "description": "Present if the server supports argument autocompletion suggestions." + }, + "experimental": { + "additionalProperties": { + "$ref": "#/$defs/JSONObject" + }, + "description": "Experimental, non-standard capabilities that the server supports.", + "type": "object" + }, + "extensions": { + "additionalProperties": { + "$ref": "#/$defs/JSONObject" + }, + "description": "Optional MCP extensions that the server supports. Keys are extension identifiers\n(e.g., \"io.modelcontextprotocol/tasks\"), and values are per-extension settings\nobjects. An empty object indicates support with no settings.\n\nKeys MUST follow the {@link MetaObject`_meta` key naming rules}, with a\nmandatory prefix.", + "type": "object" + }, + "logging": { + "$ref": "#/$defs/JSONObject", + "description": "Present if the server supports sending log messages to the client." + }, + "prompts": { + "description": "Present if the server offers any prompt templates.", + "properties": { + "listChanged": { + "description": "Whether this server supports notifications for changes to the prompt list.", + "type": "boolean" + } + }, + "type": "object" + }, + "resources": { + "description": "Present if the server offers any resources to read.", + "properties": { + "listChanged": { + "description": "Whether this server supports notifications for changes to the resource list.", + "type": "boolean" + }, + "subscribe": { + "description": "Whether this server supports subscribing to resource updates.", + "type": "boolean" + } + }, + "type": "object" + }, + "tools": { + "description": "Present if the server offers any tools to call.", + "properties": { + "listChanged": { + "description": "Whether this server supports notifications for changes to the tool list.", + "type": "boolean" + } + }, + "type": "object" + } + }, + "type": "object" + }, + "ServerNotification": { + "anyOf": [ + { + "$ref": "#/$defs/CancelledNotification" + }, + { + "$ref": "#/$defs/ProgressNotification" + }, + { + "$ref": "#/$defs/ResourceListChangedNotification" + }, + { + "$ref": "#/$defs/SubscriptionsAcknowledgedNotification" + }, + { + "$ref": "#/$defs/ResourceUpdatedNotification" + }, + { + "$ref": "#/$defs/PromptListChangedNotification" + }, + { + "$ref": "#/$defs/ToolListChangedNotification" + }, + { + "$ref": "#/$defs/LoggingMessageNotification" + } + ] + }, + "ServerResult": { + "anyOf": [ + { + "$ref": "#/$defs/Result" + }, + { + "$ref": "#/$defs/InputRequiredResult" + }, + { + "$ref": "#/$defs/DiscoverResult" + }, + { + "$ref": "#/$defs/ListResourcesResult" + }, + { + "$ref": "#/$defs/ListResourceTemplatesResult" + }, + { + "$ref": "#/$defs/ReadResourceResult" + }, + { + "$ref": "#/$defs/SubscriptionsListenResult" + }, + { + "$ref": "#/$defs/ListPromptsResult" + }, + { + "$ref": "#/$defs/GetPromptResult" + }, + { + "$ref": "#/$defs/ListToolsResult" + }, + { + "$ref": "#/$defs/CallToolResult" + }, + { + "$ref": "#/$defs/CompleteResult" + } + ] + }, + "SingleSelectEnumSchema": { + "anyOf": [ + { + "$ref": "#/$defs/UntitledSingleSelectEnumSchema" + }, + { + "$ref": "#/$defs/TitledSingleSelectEnumSchema" + } + ] + }, + "StringSchema": { + "properties": { + "default": { + "type": "string" + }, + "description": { + "type": "string" + }, + "format": { + "enum": [ + "date", + "date-time", + "email", + "uri" + ], + "type": "string" + }, + "maxLength": { + "type": "integer" + }, + "minLength": { + "type": "integer" + }, + "title": { + "type": "string" + }, + "type": { + "const": "string", + "type": "string" + } + }, + "required": [ + "type" + ], + "type": "object" + }, + "SubscriptionFilter": { + "description": "The set of notification types a client may opt in to on a\n{@link SubscriptionsListenRequestsubscriptions/listen} request.\n\nEach notification type is **opt-in**; the server **MUST NOT** send\nnotification types the client has not explicitly requested here.", + "properties": { + "promptsListChanged": { + "description": "If true, receive {@link PromptListChangedNotificationnotifications/prompts/list_changed}.", + "type": "boolean" + }, + "resourceSubscriptions": { + "description": "Subscribe to {@link ResourceUpdatedNotificationnotifications/resources/updated} for these resource URIs.\nReplaces the former `resources/subscribe` RPC.", + "items": { + "type": "string" + }, + "type": "array" + }, + "resourcesListChanged": { + "description": "If true, receive {@link ResourceListChangedNotificationnotifications/resources/list_changed}.", + "type": "boolean" + }, + "toolsListChanged": { + "description": "If true, receive {@link ToolListChangedNotificationnotifications/tools/list_changed}.", + "type": "boolean" + } + }, + "type": "object" + }, + "SubscriptionsAcknowledgedNotification": { + "description": "Sent by the server to acknowledge that a\n{@link SubscriptionsListenRequestsubscriptions/listen} subscription has been\nestablished and to report which notification types it agreed to honor.\n\nThis notification MUST be the first message the server sends carrying the\nsubscription's ID in `io.modelcontextprotocol/subscriptionId`. The server MUST\nNOT send any notification on the subscription before acknowledging it. On\nstdio, where every subscription shares one channel, this ordering is defined\nper subscription ID and not per channel: messages belonging to other\nsubscriptions MAY be interleaved before it.", + "properties": { + "jsonrpc": { + "const": "2.0", + "type": "string" + }, + "method": { + "const": "notifications/subscriptions/acknowledged", + "type": "string" + }, + "params": { + "$ref": "#/$defs/SubscriptionsAcknowledgedNotificationParams" + } + }, + "required": [ + "jsonrpc", + "method", + "params" + ], + "type": "object" + }, + "SubscriptionsAcknowledgedNotificationParams": { + "description": "Parameters for a {@link SubscriptionsAcknowledgedNotificationnotifications/subscriptions/acknowledged} notification.", + "properties": { + "_meta": { + "$ref": "#/$defs/NotificationMetaObject" + }, + "notifications": { + "$ref": "#/$defs/SubscriptionFilter", + "description": "The subset of requested notification types the server agreed to honor.\nOnly includes notification types the server actually supports; if the\nclient requested an unsupported type (e.g., `promptsListChanged` when\nthe server has no prompts), it is omitted from this set." + } + }, + "required": [ + "notifications" + ], + "type": "object" + }, + "SubscriptionsListenRequest": { + "description": "Sent from the client to open a long-lived channel for receiving notifications\noutside the context of a specific request. Replaces the previous HTTP GET\nendpoint and ensures consistent behavior between HTTP and STDIO.", + "properties": { + "id": { + "$ref": "#/$defs/RequestId" + }, + "jsonrpc": { + "const": "2.0", + "type": "string" + }, + "method": { + "const": "subscriptions/listen", + "type": "string" + }, + "params": { + "$ref": "#/$defs/SubscriptionsListenRequestParams" + } + }, + "required": [ + "id", + "jsonrpc", + "method", + "params" + ], + "type": "object" + }, + "SubscriptionsListenRequestParams": { + "description": "Parameters for a {@link SubscriptionsListenRequestsubscriptions/listen} request.", + "properties": { + "_meta": { + "$ref": "#/$defs/RequestMetaObject" + }, + "notifications": { + "$ref": "#/$defs/SubscriptionFilter", + "description": "The notifications the client opts in to on this stream. The server\n**MUST NOT** send notification types the client has not explicitly\nrequested." + } + }, + "required": [ + "_meta", + "notifications" + ], + "type": "object" + }, + "SubscriptionsListenResult": { + "description": "The response to a {@link SubscriptionsListenRequestsubscriptions/listen}\nrequest, signalling that the subscription has ended gracefully (for example,\nduring server shutdown). Because the listen stream is long-lived, this result\nis sent only when the server tears the subscription down; an abrupt transport\nclose carries no response. The result body is otherwise empty.", + "properties": { + "_meta": { + "$ref": "#/$defs/SubscriptionsListenResultMetaObject" + }, + "resultType": { + "description": "Indicates the type of the result, which allows the client to determine\nhow to parse the result object.\n\nServers implementing this protocol version MUST include this field.\nFor backward compatibility, when a client receives a result from a\nserver implementing an earlier protocol version (which does not include\n`resultType`), the client MUST treat the absent field as `\"complete\"`.", + "type": "string" + } + }, + "required": [ + "_meta", + "resultType" + ], + "type": "object" + }, + "SubscriptionsListenResultMetaObject": { + "description": "Extends {@link ResultMetaObject} with the subscription-stream identifier carried by a\n{@link SubscriptionsListenResult}. All key naming rules from `MetaObject` apply.", + "properties": { + "io.modelcontextprotocol/serverInfo": { + "$ref": "#/$defs/Implementation", + "description": "Identifies the server software producing the response. Servers SHOULD\ninclude this field on every response unless specifically configured not\nto do so.\n\nThe {@link Implementation} schema requires `name` and `version`; other\nfields are optional.\n\nThe value is self-reported by the server and is not verified by the\nprotocol. It is intended for display, logging, and debugging. Clients\nSHOULD NOT use it to change their behavior, and SHOULD NOT rely on it for\nsecurity decisions." + }, + "io.modelcontextprotocol/subscriptionId": { + "$ref": "#/$defs/RequestId", + "description": "Identifies the subscription stream this response closes, so the client can\ncorrelate it with the originating subscription — mirroring the same key on\nthe stream's notifications. The value is the JSON-RPC ID of the\n`subscriptions/listen` request that opened the stream (and equals this\nresponse's `id`)." + } + }, + "required": [ + "io.modelcontextprotocol/subscriptionId" + ], + "type": "object" + }, + "SubscriptionsListenResultResponse": { + "description": "A successful response from the server for a {@link SubscriptionsListenRequestsubscriptions/listen}\nrequest, sent when the server tears the subscription down gracefully.", + "properties": { + "id": { + "$ref": "#/$defs/RequestId" + }, + "jsonrpc": { + "const": "2.0", + "type": "string" + }, + "result": { + "$ref": "#/$defs/SubscriptionsListenResult" + } + }, + "required": [ + "id", + "jsonrpc", + "result" + ], + "type": "object" + }, + "TextContent": { + "description": "Text provided to or from an LLM.", + "properties": { + "_meta": { + "$ref": "#/$defs/MetaObject" + }, + "annotations": { + "$ref": "#/$defs/Annotations", + "description": "Optional annotations for the client." + }, + "text": { + "description": "The text content of the message.", + "type": "string" + }, + "type": { + "const": "text", + "type": "string" + } + }, + "required": [ + "text", + "type" + ], + "type": "object" + }, + "TextResourceContents": { + "properties": { + "_meta": { + "$ref": "#/$defs/MetaObject" + }, + "mimeType": { + "description": "The MIME type of this resource, if known.", + "type": "string" + }, + "text": { + "description": "The text of the item. This must only be set if the item can actually be represented as text (not binary data).", + "type": "string" + }, + "uri": { + "description": "The URI of this resource.", + "format": "uri", + "type": "string" + } + }, + "required": [ + "text", + "uri" + ], + "type": "object" + }, + "TitledMultiSelectEnumSchema": { + "description": "Schema for multiple-selection enumeration with display titles for each option.", + "properties": { + "default": { + "description": "Optional default value.", + "items": { + "type": "string" + }, + "type": "array" + }, + "description": { + "description": "Optional description for the enum field.", + "type": "string" + }, + "items": { + "description": "Schema for array items with enum options and display labels.", + "properties": { + "anyOf": { + "description": "Array of enum options with values and display labels.", + "items": { + "properties": { + "const": { + "description": "The constant enum value.", + "type": "string" + }, + "title": { + "description": "Display title for this option.", + "type": "string" + } + }, + "required": [ + "const", + "title" + ], + "type": "object" + }, + "type": "array" + } + }, + "required": [ + "anyOf" + ], + "type": "object" + }, + "maxItems": { + "description": "Maximum number of items to select.", + "type": "integer" + }, + "minItems": { + "description": "Minimum number of items to select.", + "type": "integer" + }, + "title": { + "description": "Optional title for the enum field.", + "type": "string" + }, + "type": { + "const": "array", + "type": "string" + } + }, + "required": [ + "items", + "type" + ], + "type": "object" + }, + "TitledSingleSelectEnumSchema": { + "description": "Schema for single-selection enumeration with display titles for each option.", + "properties": { + "default": { + "description": "Optional default value.", + "type": "string" + }, + "description": { + "description": "Optional description for the enum field.", + "type": "string" + }, + "oneOf": { + "description": "Array of enum options with values and display labels.", + "items": { + "properties": { + "const": { + "description": "The enum value.", + "type": "string" + }, + "title": { + "description": "Display label for this option.", + "type": "string" + } + }, + "required": [ + "const", + "title" + ], + "type": "object" + }, + "type": "array" + }, + "title": { + "description": "Optional title for the enum field.", + "type": "string" + }, + "type": { + "const": "string", + "type": "string" + } + }, + "required": [ + "oneOf", + "type" + ], + "type": "object" + }, + "Tool": { + "description": "Definition for a tool the client can call.", + "properties": { + "_meta": { + "$ref": "#/$defs/MetaObject" + }, + "annotations": { + "$ref": "#/$defs/ToolAnnotations", + "description": "Optional additional tool information.\n\nDisplay name precedence order is: `title`, `annotations.title`, then `name`." + }, + "description": { + "description": "A human-readable description of the tool.\n\nThis can be used by clients to improve the LLM's understanding of available tools. It can be thought of like a \"hint\" to the model.", + "type": "string" + }, + "icons": { + "description": "Optional set of sized icons that the client can display in a user interface.\n\nClients that support rendering icons MUST support at least the following MIME types:\n- `image/png` - PNG images (safe, universal compatibility)\n- `image/jpeg` (and `image/jpg`) - JPEG images (safe, universal compatibility)\n\nClients that support rendering icons SHOULD also support:\n- `image/svg+xml` - SVG images (scalable but requires security precautions)\n- `image/webp` - WebP images (modern, efficient format)", + "items": { + "$ref": "#/$defs/Icon" + }, + "type": "array" + }, + "inputSchema": { + "additionalProperties": {}, + "description": "A JSON Schema object defining the expected parameters for the tool.\n\nTool arguments are always JSON objects, so `type: \"object\"` is required at the root.\nBeyond that, any JSON Schema 2020-12 keyword may appear alongside `type` — including\ncomposition keywords (`oneOf`, `anyOf`, `allOf`, `not`), conditional keywords\n(`if`/`then`/`else`), reference keywords (`$ref`, `$defs`, `$anchor`), and any other\nstandard validation or annotation keywords.\n\nProperty schemas may carry an `x-mcp-header` annotation to mirror the\nargument value into an HTTP header on the Streamable HTTP transport. See\nthe Streamable HTTP transport specification for the validity and\nextraction rules.\n\nDefaults to JSON Schema 2020-12 when no explicit `$schema` is provided.", + "properties": { + "$schema": { + "type": "string" + }, + "type": { + "const": "object", + "type": "string" + } + }, + "required": [ + "type" + ], + "type": "object" + }, + "name": { + "description": "Intended for programmatic or logical use, but used as a display name in past specs or fallback (if title isn't present).", + "type": "string" + }, + "outputSchema": { + "additionalProperties": {}, + "description": "An optional JSON Schema object defining the structure of the tool's output returned in\nthe structuredContent field of a {@link CallToolResult}. This can be any valid JSON Schema 2020-12.\n\nDefaults to JSON Schema 2020-12 when no explicit `$schema` is provided.", + "properties": { + "$schema": { + "type": "string" + } + }, + "type": "object" + }, + "title": { + "description": "Intended for UI and end-user contexts — optimized to be human-readable and easily understood,\neven by those unfamiliar with domain-specific terminology.\n\nIf not provided, the name should be used for display (except for {@link Tool},\nwhere `annotations.title` should be given precedence over using `name`,\nif present).", + "type": "string" + } + }, + "required": [ + "inputSchema", + "name" + ], + "type": "object" + }, + "ToolAnnotations": { + "description": "Additional properties describing a {@link Tool} to clients.\n\nNOTE: all properties in `ToolAnnotations` are **hints**.\nThey are not guaranteed to provide a faithful description of\ntool behavior (including descriptive properties like `title`).\n\nClients should never make tool use decisions based on `ToolAnnotations`\nreceived from untrusted servers.", + "properties": { + "destructiveHint": { + "description": "If true, the tool may perform destructive updates to its environment.\nIf false, the tool performs only additive updates.\n\n(This property is meaningful only when `readOnlyHint == false`)\n\nDefault: true", + "type": "boolean" + }, + "idempotentHint": { + "description": "If true, calling the tool repeatedly with the same arguments\nwill have no additional effect on its environment.\n\n(This property is meaningful only when `readOnlyHint == false`)\n\nDefault: false", + "type": "boolean" + }, + "openWorldHint": { + "description": "If true, this tool may interact with an \"open world\" of external\nentities. If false, the tool's domain of interaction is closed.\nFor example, the world of a web search tool is open, whereas that\nof a memory tool is not.\n\nDefault: true", + "type": "boolean" + }, + "readOnlyHint": { + "description": "If true, the tool does not modify its environment.\n\nDefault: false", + "type": "boolean" + }, + "title": { + "description": "A human-readable title for the tool.", + "type": "string" + } + }, + "type": "object" + }, + "ToolChoice": { + "description": "Controls tool selection behavior for sampling requests.", + "properties": { + "mode": { + "description": "Controls the tool use ability of the model:\n- `\"auto\"`: Model decides whether to use tools (default)\n- `\"required\"`: Model MUST use at least one tool before completing\n- `\"none\"`: Model MUST NOT use any tools", + "enum": [ + "auto", + "none", + "required" + ], + "type": "string" + } + }, + "type": "object" + }, + "ToolListChangedNotification": { + "description": "An optional notification from the server to the client, informing it that the list of tools it offers has changed. This is only delivered on a {@link SubscriptionsListenRequestsubscriptions/listen} stream when the client requested it via the `toolsListChanged` filter field.", + "properties": { + "jsonrpc": { + "const": "2.0", + "type": "string" + }, + "method": { + "const": "notifications/tools/list_changed", + "type": "string" + }, + "params": { + "$ref": "#/$defs/NotificationParams" + } + }, + "required": [ + "jsonrpc", + "method" + ], + "type": "object" + }, + "ToolResultContent": { + "description": "The result of a tool use, provided by the user back to the assistant.", + "properties": { + "_meta": { + "$ref": "#/$defs/MetaObject", + "description": "Optional metadata about the tool result. Clients SHOULD preserve this field when\nincluding tool results in subsequent sampling requests to enable caching optimizations." + }, + "content": { + "description": "The unstructured result content of the tool use.\n\nThis has the same format as {@link CallToolResult.content} and can include text, images,\naudio, resource links, and embedded resources.", + "items": { + "$ref": "#/$defs/ContentBlock" + }, + "type": "array" + }, + "isError": { + "description": "Whether the tool use resulted in an error.\n\nIf true, the content typically describes the error that occurred.\nDefault: false", + "type": "boolean" + }, + "structuredContent": { + "description": "An optional structured result value.\n\nThis can be any JSON value (object, array, string, number, boolean, or null).\nIf the tool defined an {@link Tool.outputSchema}, this SHOULD conform to that schema." + }, + "toolUseId": { + "description": "The ID of the tool use this result corresponds to.\n\nThis MUST match the ID from a previous {@link ToolUseContent}.", + "type": "string" + }, + "type": { + "const": "tool_result", + "type": "string" + } + }, + "required": [ + "content", + "toolUseId", + "type" + ], + "type": "object" + }, + "ToolUseContent": { + "description": "A request from the assistant to call a tool.", + "properties": { + "_meta": { + "$ref": "#/$defs/MetaObject", + "description": "Optional metadata about the tool use. Clients SHOULD preserve this field when\nincluding tool uses in subsequent sampling requests to enable caching optimizations." + }, + "id": { + "description": "A unique identifier for this tool use.\n\nThis ID is used to match tool results to their corresponding tool uses.", + "type": "string" + }, + "input": { + "additionalProperties": {}, + "description": "The arguments to pass to the tool, conforming to the tool's input schema.", + "type": "object" + }, + "name": { + "description": "The name of the tool to call.", + "type": "string" + }, + "type": { + "const": "tool_use", + "type": "string" + } + }, + "required": [ + "id", + "input", + "name", + "type" + ], + "type": "object" + }, + "UnsupportedProtocolVersionError": { + "description": "Returned when the request's protocol version is unknown to the server or\nunsupported (e.g., a known experimental or draft version the server has\nchosen not to implement). For HTTP, the response status code MUST be\n`400 Bad Request`.", + "properties": { + "error": { + "allOf": [ + { + "$ref": "#/$defs/Error" + }, + { + "properties": { + "code": { + "const": -32022, + "type": "integer" + }, + "data": { + "properties": { + "requested": { + "description": "The protocol version that was requested by the client.", + "type": "string" + }, + "supported": { + "description": "Protocol versions the server supports. The client should choose a\nmutually supported version from this list and retry.", + "items": { + "type": "string" + }, + "type": "array" + } + }, + "required": [ + "requested", + "supported" + ], + "type": "object" + } + }, + "required": [ + "code", + "data" + ], + "type": "object" + } + ] + }, + "id": { + "$ref": "#/$defs/RequestId" + }, + "jsonrpc": { + "const": "2.0", + "type": "string" + } + }, + "required": [ + "error", + "jsonrpc" + ], + "type": "object" + }, + "UntitledMultiSelectEnumSchema": { + "description": "Schema for multiple-selection enumeration without display titles for options.", + "properties": { + "default": { + "description": "Optional default value.", + "items": { + "type": "string" + }, + "type": "array" + }, + "description": { + "description": "Optional description for the enum field.", + "type": "string" + }, + "items": { + "description": "Schema for the array items.", + "properties": { + "enum": { + "description": "Array of enum values to choose from.", + "items": { + "type": "string" + }, + "type": "array" + }, + "type": { + "const": "string", + "type": "string" + } + }, + "required": [ + "enum", + "type" + ], + "type": "object" + }, + "maxItems": { + "description": "Maximum number of items to select.", + "type": "integer" + }, + "minItems": { + "description": "Minimum number of items to select.", + "type": "integer" + }, + "title": { + "description": "Optional title for the enum field.", + "type": "string" + }, + "type": { + "const": "array", + "type": "string" + } + }, + "required": [ + "items", + "type" + ], + "type": "object" + }, + "UntitledSingleSelectEnumSchema": { + "description": "Schema for single-selection enumeration without display titles for options.", + "properties": { + "default": { + "description": "Optional default value.", + "type": "string" + }, + "description": { + "description": "Optional description for the enum field.", + "type": "string" + }, + "enum": { + "description": "Array of enum values to choose from.", + "items": { + "type": "string" + }, + "type": "array" + }, + "title": { + "description": "Optional title for the enum field.", + "type": "string" + }, + "type": { + "const": "string", + "type": "string" + } + }, + "required": [ + "enum", + "type" + ], + "type": "object" + } + } +} + diff --git a/src/spec-types/2026-07-28.ts b/src/spec-types/2026-07-28.ts new file mode 100644 index 00000000..9b55feeb --- /dev/null +++ b/src/spec-types/2026-07-28.ts @@ -0,0 +1,3197 @@ +/* JSON types */ + +/** + * @category Common Types + */ +export type JSONValue = + string | number | boolean | null | JSONObject | JSONArray; + +/** + * @category Common Types + */ +export type JSONObject = { [key: string]: JSONValue }; + +/** + * @category Common Types + */ +export type JSONArray = JSONValue[]; + +/* JSON-RPC types */ + +/** + * Refers to any valid JSON-RPC object that can be decoded off the wire, or encoded to be sent. + * + * @category JSON-RPC + */ +export type JSONRPCMessage = + JSONRPCRequest | JSONRPCNotification | JSONRPCResponse; + +/** @internal */ +export const LATEST_PROTOCOL_VERSION = "2026-07-28"; +/** @internal */ +export const JSONRPC_VERSION = "2.0"; + +/** + * Represents the contents of a `_meta` field, which clients and servers use to attach additional metadata to their interactions. + * + * Certain key names are reserved by MCP for protocol-level metadata; implementations MUST NOT make assumptions about values at these keys. Additionally, specific schema definitions may reserve particular names for purpose-specific metadata, as declared in those definitions. + * + * Valid keys have two segments: + * + * **Prefix:** + * - Optional — if specified, MUST be a series of _labels_ separated by dots (`.`), followed by a slash (`/`). + * - Labels MUST start with a letter and end with a letter or digit. Interior characters may be letters, digits, or hyphens (`-`). + * - Implementations SHOULD use reverse DNS notation (e.g., `com.example/` rather than `example.com/`). + * - Any prefix where the second label is `modelcontextprotocol` or `mcp` is **reserved** for MCP use. For example: `io.modelcontextprotocol/`, `dev.mcp/`, `org.modelcontextprotocol.api/`, and `com.mcp.tools/` are all reserved. However, `com.example.mcp/` is NOT reserved, as the second label is `example`. + * + * **Name:** + * - Unless empty, MUST start and end with an alphanumeric character (`[a-z0-9A-Z]`). + * - Interior characters may be alphanumeric, hyphens (`-`), underscores (`_`), or dots (`.`). + * + * @see [General fields: `_meta`](/specification/2026-07-28/basic/index#meta) for more details. + * @category Common Types + */ +export type MetaObject = Record; + +/** + * Extends {@link MetaObject} with additional request-specific fields. All key naming rules from `MetaObject` apply. + * + * @see {@link MetaObject} for key naming rules and reserved prefixes. + * @see [General fields: `_meta`](/specification/2026-07-28/basic/index#meta) for more details. + * @category Common Types + */ +export interface RequestMetaObject extends MetaObject { + /** + * If specified, the caller is requesting out-of-band progress notifications for this request (as represented by {@link ProgressNotification | notifications/progress}). The value of this parameter is an opaque token that will be attached to any subsequent notifications. The receiver is not obligated to provide these notifications. + */ + progressToken?: ProgressToken; + /** + * The MCP Protocol Version being used for this request. Required. + * + * For the HTTP transport, this value MUST match the `MCP-Protocol-Version` + * header; otherwise the server MUST return a `400 Bad Request`. If the + * server does not support the requested version, it MUST return an + * {@link UnsupportedProtocolVersionError}. + */ + "io.modelcontextprotocol/protocolVersion": string; + /** + * Identifies the client software making the request. Clients SHOULD + * include this field on every request unless specifically configured not + * to do so. + * + * The {@link Implementation} schema requires `name` and `version`; other + * fields are optional. + * + * The value is self-reported by the client and is not verified by the + * protocol. It is intended for display, logging, and debugging. Servers + * SHOULD NOT use it to change their behavior, and SHOULD NOT rely on it for + * security decisions. + */ + "io.modelcontextprotocol/clientInfo"?: Implementation; + /** + * The client's capabilities for this specific request. Required. + * + * Capabilities are declared per-request rather than once at initialization; + * an empty object means the client supports no optional capabilities. + * Servers MUST NOT infer capabilities from prior requests. + */ + "io.modelcontextprotocol/clientCapabilities": ClientCapabilities; + /** + * The desired log level for this request. Optional. + * + * If absent, the server MUST NOT send any {@link LoggingMessageNotification | notifications/message} + * notifications for this request. The client opts in to log messages by + * explicitly setting a level. Replaces the former `logging/setLevel` RPC. + * + * @deprecated Deprecated as of protocol version 2026-07-28 (SEP-2577). + * Remains in the specification for at least twelve months; see the + * deprecated features registry. + */ + "io.modelcontextprotocol/logLevel"?: LoggingLevel; +} + +/** + * Extends {@link MetaObject} with additional notification-specific fields. All key naming rules from `MetaObject` apply. + * + * @see {@link MetaObject} for key naming rules and reserved prefixes. + * @see [General fields: `_meta`](/specification/2026-07-28/basic/index#meta) for more details. + * @category Common Types + */ +export interface NotificationMetaObject extends MetaObject { + /** + * Identifies the subscription stream a notification was delivered on. The + * server MUST include this key on every notification delivered via a + * {@link SubscriptionsListenRequest | subscriptions/listen} stream, so the + * client can correlate the notification with the originating subscription. + * The key is absent on notifications not delivered via a subscription + * stream (e.g. progress notifications for an in-flight request), which is + * why it is optional here. + * + * The value is the JSON-RPC ID of the `subscriptions/listen` request that + * opened the stream. + */ + "io.modelcontextprotocol/subscriptionId"?: RequestId; +} + +/** + * Extends {@link MetaObject} with additional result-specific fields. All key naming rules from `MetaObject` apply. + * + * @see {@link MetaObject} for key naming rules and reserved prefixes. + * @see [General fields: `_meta`](/specification/2026-07-28/basic/index#meta) for more details. + * @category Common Types + */ +export interface ResultMetaObject extends MetaObject { + /** + * Identifies the server software producing the response. Servers SHOULD + * include this field on every response unless specifically configured not + * to do so. + * + * The {@link Implementation} schema requires `name` and `version`; other + * fields are optional. + * + * The value is self-reported by the server and is not verified by the + * protocol. It is intended for display, logging, and debugging. Clients + * SHOULD NOT use it to change their behavior, and SHOULD NOT rely on it for + * security decisions. + */ + "io.modelcontextprotocol/serverInfo"?: Implementation; +} + +/** + * A progress token, used to associate progress notifications with the original request. + * + * @category Common Types + */ +export type ProgressToken = string | number; + +/** + * An opaque token used to represent a cursor for pagination. + * + * @category Common Types + */ +export type Cursor = string; + +/** + * Common params for any request. + * + * @category Common Types + */ +export interface RequestParams { + _meta: RequestMetaObject; +} + +/** @internal */ +export interface Request { + method: string; + // Allow unofficial extensions of `Request.params` without impacting `RequestParams`. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + params?: { [key: string]: any }; +} + +/** + * Common params for any notification. + * + * @category Common Types + */ +export interface NotificationParams { + _meta?: NotificationMetaObject; +} + +/** @internal */ +export interface Notification { + method: string; + // Allow unofficial extensions of `Notification.params` without impacting `NotificationParams`. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + params?: { [key: string]: any }; +} + +/** + * Indicates the type of a {@link Result} object, allowing the client to + * determine how to parse the response. + * + * complete - the request completed successfully and the result contains the final content. + * input_required - the request requires additional input and the result contains an {@link InputRequiredResult} object with instructions for the client to provide additional input before retrying the original request. + * @category Common Types + */ +export type ResultType = "complete" | "input_required" | string; + +/** + * Common result fields. + * + * @category Common Types + */ +export interface Result { + _meta?: ResultMetaObject; + /** + * Indicates the type of the result, which allows the client to determine + * how to parse the result object. + * + * Servers implementing this protocol version MUST include this field. + * For backward compatibility, when a client receives a result from a + * server implementing an earlier protocol version (which does not include + * `resultType`), the client MUST treat the absent field as `"complete"`. + */ + resultType: ResultType; + [key: string]: unknown; +} + +/** + * @category Errors + */ +export interface Error { + /** + * The error type that occurred. + */ + code: number; + /** + * A short description of the error. The message SHOULD be limited to a concise single sentence. + */ + message: string; + /** + * Additional information about the error. The value of this member is defined by the sender (e.g. detailed error information, nested errors etc.). + */ + data?: unknown; +} + +/** + * A uniquely identifying ID for a request in JSON-RPC. + * + * @category Common Types + */ +export type RequestId = string | number; + +/** + * A request that expects a response. + * + * @category JSON-RPC + */ +export interface JSONRPCRequest extends Request { + jsonrpc: typeof JSONRPC_VERSION; + id: RequestId; +} + +/** + * A notification which does not expect a response. + * + * @category JSON-RPC + */ +export interface JSONRPCNotification extends Notification { + jsonrpc: typeof JSONRPC_VERSION; +} + +/** + * A successful (non-error) response to a request. + * + * @category JSON-RPC + */ +export interface JSONRPCResultResponse { + jsonrpc: typeof JSONRPC_VERSION; + id: RequestId; + result: Result; +} + +/** + * A response to a request that indicates an error occurred. + * + * @category JSON-RPC + */ +export interface JSONRPCErrorResponse { + jsonrpc: typeof JSONRPC_VERSION; + id?: RequestId; + error: Error; +} + +/** + * A response to a request, containing either the result or error. + * + * @category JSON-RPC + */ +export type JSONRPCResponse = JSONRPCResultResponse | JSONRPCErrorResponse; + +// Standard JSON-RPC error codes +export const PARSE_ERROR = -32700; +export const INVALID_REQUEST = -32600; +export const METHOD_NOT_FOUND = -32601; +export const INVALID_PARAMS = -32602; +export const INTERNAL_ERROR = -32603; + +/** + * A JSON-RPC error indicating that invalid JSON was received by the server. This error is returned when the server cannot parse the JSON text of a message. + * + * @see {@link https://www.jsonrpc.org/specification#error_object | JSON-RPC 2.0 Error Object} + * + * @example Invalid JSON + * {@includeCode ./examples/ParseError/invalid-json.json} + * + * @category Errors + */ +export interface ParseError extends Error { + code: typeof PARSE_ERROR; +} + +/** + * A JSON-RPC error indicating that the request is not a valid request object. This error is returned when the message structure does not conform to the JSON-RPC 2.0 specification requirements for a request (e.g., missing required fields like `jsonrpc` or `method`, or using invalid types for these fields). + * + * @see {@link https://www.jsonrpc.org/specification#error_object | JSON-RPC 2.0 Error Object} + * + * @category Errors + */ +export interface InvalidRequestError extends Error { + code: typeof INVALID_REQUEST; +} + +/** + * A JSON-RPC error indicating that the requested method does not exist or is not available. + * + * In MCP, a server returns this error when a client invokes a method the server does not implement — either a genuinely unknown method, or one gated behind a server capability the server did not advertise (e.g., calling `prompts/list` when the `prompts` capability was not advertised). + * + * A request that requires a client capability the client did not declare is signalled instead by {@link MissingRequiredClientCapabilityError} (`-32021`). + * + * @see {@link https://www.jsonrpc.org/specification#error_object | JSON-RPC 2.0 Error Object} + * + * @example Prompts not supported + * {@includeCode ./examples/MethodNotFoundError/prompts-not-supported.json} + * + * @category Errors + */ +export interface MethodNotFoundError extends Error { + code: typeof METHOD_NOT_FOUND; +} + +/** + * A JSON-RPC error indicating that the method parameters are invalid or malformed. + * + * In MCP, this error is returned in various contexts when request parameters fail validation: + * + * - **Tools**: Unknown tool name or invalid tool arguments + * - **Prompts**: Unknown prompt name or missing required arguments + * - **Pagination**: Invalid or expired cursor values + * - **Logging**: Invalid log level + * - **Elicitation**: Server requests an elicitation mode not declared in client capabilities + * - **Sampling**: Missing tool result or tool results mixed with other content + * + * @see {@link https://www.jsonrpc.org/specification#error_object | JSON-RPC 2.0 Error Object} + * + * @example Unknown tool + * {@includeCode ./examples/InvalidParamsError/unknown-tool.json} + * + * @example Invalid tool arguments + * {@includeCode ./examples/InvalidParamsError/invalid-tool-arguments.json} + * + * @example Unknown prompt + * {@includeCode ./examples/InvalidParamsError/unknown-prompt.json} + * + * @example Invalid cursor + * {@includeCode ./examples/InvalidParamsError/invalid-cursor.json} + * + * @category Errors + */ +export interface InvalidParamsError extends Error { + code: typeof INVALID_PARAMS; +} + +/** + * A JSON-RPC error indicating that an internal error occurred on the receiver. This error is returned when the receiver encounters an unexpected condition that prevents it from fulfilling the request. + * + * @see {@link https://www.jsonrpc.org/specification#error_object | JSON-RPC 2.0 Error Object} + * + * @example Unexpected error + * {@includeCode ./examples/InternalError/unexpected-error.json} + * + * @category Errors + */ +export interface InternalError extends Error { + code: typeof INTERNAL_ERROR; +} + +/* + * MCP error codes. + * + * JSON-RPC 2.0 reserves `-32000` to `-32099` for implementation-defined + * server errors. MCP partitions that range: + * + * - `-32000` to `-32019`: implementation-defined. Existing SDKs and + * implementations use codes here for their own purposes; the specification + * will never define codes in this sub-range, and receivers must not assign + * cross-implementation semantics to them. + * - `-32020` to `-32099`: reserved for error codes defined by the MCP + * specification. Every code allocated here is recorded in this file. + * Codes are allocated sequentially starting at `-32020` and proceeding + * toward `-32099`. + * + * Codes defined by earlier protocol versions remain reserved and are never + * reused: `-32002` (resource not found, 2025-11-25 and earlier; replaced by + * `-32602`) and `-32042` (URL elicitation required, 2025-11-25 only). + */ + +/** + * Error code returned when the HTTP headers of a request do not match the + * corresponding values in the request body, or required headers are + * missing or malformed. + * + * @category Errors + */ +export const HEADER_MISMATCH = -32020; + +/** + * Error code returned when a server requires a client capability that was + * not declared in the request's `clientCapabilities`. + * + * @category Errors + */ +export const MISSING_REQUIRED_CLIENT_CAPABILITY = -32021; + +/** + * Error code returned when the request's protocol version is not supported + * by the server. + * + * @category Errors + */ +export const UNSUPPORTED_PROTOCOL_VERSION = -32022; + +/** + * Returned when a server rejects a request because the values in the HTTP + * headers do not match the corresponding values in the request body, or + * because required headers are missing or malformed. For HTTP, the response + * status code MUST be `400 Bad Request`. + * + * @example Header mismatch + * {@includeCode ./examples/HeaderMismatchError/header-mismatch.json} + * + * @category Errors + */ +export interface HeaderMismatchError extends Omit< + JSONRPCErrorResponse, + "error" +> { + error: Error & { + code: typeof HEADER_MISMATCH; + }; +} + +/** + * Returned when the request's protocol version is unknown to the server or + * unsupported (e.g., a known experimental or draft version the server has + * chosen not to implement). For HTTP, the response status code MUST be + * `400 Bad Request`. + * + * @example Unsupported protocol version + * {@includeCode ./examples/UnsupportedProtocolVersionError/unsupported-version.json} + * + * @category Errors + */ +export interface UnsupportedProtocolVersionError extends Omit< + JSONRPCErrorResponse, + "error" +> { + error: Error & { + code: typeof UNSUPPORTED_PROTOCOL_VERSION; + data: { + /** + * Protocol versions the server supports. The client should choose a + * mutually supported version from this list and retry. + */ + supported: string[]; + /** + * The protocol version that was requested by the client. + */ + requested: string; + }; + }; +} + +/** + * Returned when processing a request requires a capability the client did not + * declare in `clientCapabilities`. For HTTP, the response status code MUST be + * `400 Bad Request`. + * + * @example Missing elicitation capability + * {@includeCode ./examples/MissingRequiredClientCapabilityError/missing-elicitation-capability.json} + * + * @category Errors + */ +export interface MissingRequiredClientCapabilityError extends Omit< + JSONRPCErrorResponse, + "error" +> { + error: Error & { + code: typeof MISSING_REQUIRED_CLIENT_CAPABILITY; + data: { + /** + * The capabilities the server requires from the client to process this request. + */ + requiredCapabilities: ClientCapabilities; + }; + }; +} + +/* Empty result */ +/** + * A result that indicates success but carries no data. + * + * @category Common Types + */ +export type EmptyResult = Result; + +/** @internal */ +export type InputRequest = + CreateMessageRequest | ListRootsRequest | ElicitRequest; + +/** @internal */ +export type InputResponse = + CreateMessageResult | ListRootsResult | ElicitResult; + +/** + * A map of server-initiated requests that the client must fulfill. + * Keys are server-assigned identifiers; values are the request objects. + * + * @example Elicitation and sampling input requests + * {@includeCode ./examples/InputRequests/elicitation-and-sampling-input-requests.json} + * + * @category Multi Round-Trip + */ +export interface InputRequests { + [key: string]: InputRequest; +} + +/** + * A map of client responses to server-initiated requests. + * Keys correspond to the keys in the {@link InputRequests} map; + * values are the client's result for each request. + * + * @example Elicitation and sampling input responses + * {@includeCode ./examples/InputResponses/elicitation-and-sampling-input-responses.json} + * + * @category Multi Round-Trip + */ +export interface InputResponses { + [key: string]: InputResponse; +} + +/** + * An InputRequiredResult sent by the server to indicate that additional input is needed + * before the request can be completed. + * + * At least one of `inputRequests` or `requestState` MUST be present. + * @example InputRequiredResult with elicitation and sampling input requests and request state + * {@includeCode ./examples/InputRequiredResult/input-required-result-with-elicitation-and-sampling-and-request-state.json} + * + * @example InputRequiredResult with request state only (load shedding) + * {@includeCode ./examples/InputRequiredResult/input-required-result-with-request-state-only.json} + * + * @category Multi Round-Trip + */ +export interface InputRequiredResult extends Result { + /* Requests issued by the server that must be complete before the + * client can retry the original request. + */ + inputRequests?: InputRequests; + /* Request state to be passed back to the server when the client + * retries the original request. + * Note: The client must treat this as an opaque blob; it must not + * interpret it in any way. + */ + requestState?: string; +} + +/* Request parameter type that includes input responses and request state. + * These parameters may be included in any client-initiated request. + */ +export interface InputResponseRequestParams extends RequestParams { + /* New field to carry the responses for the server's requests from the + * InputRequiredResult message. For each key in the response's inputRequests + * field, the same key must appear here with the associated response. + */ + inputResponses?: InputResponses; + /* Request state passed back to the server from the client. + */ + requestState?: string; +} + +/* Cancellation */ +/** + * Parameters for a `notifications/cancelled` notification. + * + * @example User-requested cancellation + * {@includeCode ./examples/CancelledNotificationParams/user-requested-cancellation.json} + * + * @category `notifications/cancelled` + */ +export interface CancelledNotificationParams extends NotificationParams { + /** + * The ID of the request to cancel. + * + * This MUST correspond to the ID of a request the client previously issued. + */ + requestId: RequestId; + + /** + * An optional string describing the reason for the cancellation. This MAY be logged or presented to the user. + */ + reason?: string; +} + +/** + * This notification is sent by the client to indicate that it is cancelling a request it previously issued. + * + * On stdio, the server also sends this notification, solely to terminate a {@link SubscriptionsListenRequest | subscriptions/listen} stream: it references the ID of the `subscriptions/listen` request that opened the stream. Servers MUST NOT use this notification to cancel any other request. + * + * The request SHOULD still be in-flight, but due to communication latency, it is always possible that this notification MAY arrive after the request has already finished. + * + * This notification indicates that the result will be unused, so any associated processing SHOULD cease. + * + * @example User-requested cancellation + * {@includeCode ./examples/CancelledNotification/user-requested-cancellation.json} + * + * @category `notifications/cancelled` + */ +export interface CancelledNotification extends JSONRPCNotification { + method: "notifications/cancelled"; + params: CancelledNotificationParams; +} + +/* Discovery */ +/** + * A request from the client asking the server to advertise its supported + * protocol versions, capabilities, and other metadata. Servers **MUST** + * implement `server/discover`. Clients **MAY** call it but are not required + * to — version negotiation can also happen inline via per-request `_meta`. + * + * @example Discover request + * {@includeCode ./examples/DiscoverRequest/server-discover-request.json} + * + * @category `server/discover` + */ +export interface DiscoverRequest extends JSONRPCRequest { + method: "server/discover"; + params: RequestParams; +} + +/** + * The result returned by the server for a {@link DiscoverRequest | server/discover} request. + * + * @example Server capabilities discovery + * {@includeCode ./examples/DiscoverResult/server-capabilities-discovery.json} + * + * @category `server/discover` + */ +export interface DiscoverResult extends CacheableResult { + /** + * MCP Protocol Versions this server supports. The client should choose a + * version from this list for use in subsequent requests. + */ + supportedVersions: string[]; + /** + * The capabilities of the server. + */ + capabilities: ServerCapabilities; + /** + * Natural-language guidance describing the server and its features. + * + * This can be used by clients to improve an LLM's understanding of + * available tools (e.g., by including it in a system prompt). It should + * focus on information that helps the model use the server effectively + * and should not duplicate information already in tool descriptions. + */ + instructions?: string; +} + +/** + * A successful response from the server for a {@link DiscoverRequest | server/discover} request. + * + * @example Discover result response + * {@includeCode ./examples/DiscoverResultResponse/discover-result-response.json} + * + * @category `server/discover` + */ +export interface DiscoverResultResponse extends JSONRPCResultResponse { + result: DiscoverResult; +} + +/** + * Capabilities a client may support. Known capabilities are defined here, in this schema, but this is not a closed set: any client can define its own, additional capabilities. + * + * @category `server/discover` + */ +export interface ClientCapabilities { + /** + * Experimental, non-standard capabilities that the client supports. + */ + experimental?: { [key: string]: JSONObject }; + /** + * Present if the client supports listing roots. + * + * @deprecated Deprecated as of protocol version 2026-07-28 (SEP-2577). + * Remains in the specification for at least twelve months; see the + * deprecated features registry. + * + * @example Roots — minimum baseline support + * {@includeCode ./examples/ClientCapabilities/roots-minimum-baseline-support.json} + */ + // eslint-disable-next-line @typescript-eslint/no-empty-object-type + roots?: {}; + /** + * Present if the client supports sampling from an LLM. + * + * @deprecated Deprecated as of protocol version 2026-07-28 (SEP-2577). + * Remains in the specification for at least twelve months; see the + * deprecated features registry. + * + * @example Sampling — minimum baseline support + * {@includeCode ./examples/ClientCapabilities/sampling-minimum-baseline-support.json} + * + * @example Sampling — tool use support + * {@includeCode ./examples/ClientCapabilities/sampling-tool-use-support.json} + * + * @example Sampling — context inclusion support (deprecated) + * {@includeCode ./examples/ClientCapabilities/sampling-context-inclusion-support-deprecated.json} + */ + sampling?: { + /** + * Whether the client supports context inclusion via `includeContext` parameter. + * If not declared, servers SHOULD only use `includeContext: "none"` (or omit it). + */ + context?: JSONObject; + /** + * Whether the client supports tool use via `tools` and `toolChoice` parameters. + */ + tools?: JSONObject; + }; + /** + * Present if the client supports elicitation from the server. + * + * @example Elicitation — form and URL mode support + * {@includeCode ./examples/ClientCapabilities/elicitation-form-and-url-mode-support.json} + * + * @example Elicitation — form mode only (implicit) + * {@includeCode ./examples/ClientCapabilities/elicitation-form-only-implicit.json} + */ + elicitation?: { + form?: JSONObject; + url?: JSONObject; + }; + + /** + * Optional MCP extensions that the client supports. Keys are extension identifiers + * (e.g., "io.modelcontextprotocol/oauth-client-credentials"), and values are + * per-extension settings objects. An empty object indicates support with no settings. + * + * Keys MUST follow the {@link MetaObject | `_meta` key naming rules}, with a + * mandatory prefix. + * + * @example Extensions — MCP Apps (UI) extension with MIME type support + * {@includeCode ./examples/ClientCapabilities/extensions-ui-mime-types.json} + */ + extensions?: { [key: string]: JSONObject }; +} + +/** + * Capabilities that a server may support. Known capabilities are defined here, in this schema, but this is not a closed set: any server can define its own, additional capabilities. + * + * @category `server/discover` + */ +export interface ServerCapabilities { + /** + * Experimental, non-standard capabilities that the server supports. + */ + experimental?: { [key: string]: JSONObject }; + /** + * Present if the server supports sending log messages to the client. + * + * @deprecated Deprecated as of protocol version 2026-07-28 (SEP-2577). + * Remains in the specification for at least twelve months; see the + * deprecated features registry. + * + * @example Logging — minimum baseline support + * {@includeCode ./examples/ServerCapabilities/logging-minimum-baseline-support.json} + */ + logging?: JSONObject; + /** + * Present if the server supports argument autocompletion suggestions. + * + * @example Completions — minimum baseline support + * {@includeCode ./examples/ServerCapabilities/completions-minimum-baseline-support.json} + */ + completions?: JSONObject; + /** + * Present if the server offers any prompt templates. + * + * @example Prompts — minimum baseline support + * {@includeCode ./examples/ServerCapabilities/prompts-minimum-baseline-support.json} + * + * @example Prompts — list changed notifications + * {@includeCode ./examples/ServerCapabilities/prompts-list-changed-notifications.json} + */ + prompts?: { + /** + * Whether this server supports notifications for changes to the prompt list. + */ + listChanged?: boolean; + }; + /** + * Present if the server offers any resources to read. + * + * @example Resources — minimum baseline support + * {@includeCode ./examples/ServerCapabilities/resources-minimum-baseline-support.json} + * + * @example Resources — subscription to individual resource updates (only) + * {@includeCode ./examples/ServerCapabilities/resources-subscription-to-individual-resource-updates-only.json} + * + * @example Resources — list changed notifications (only) + * {@includeCode ./examples/ServerCapabilities/resources-list-changed-notifications-only.json} + * + * @example Resources — all notifications + * {@includeCode ./examples/ServerCapabilities/resources-all-notifications.json} + */ + resources?: { + /** + * Whether this server supports subscribing to resource updates. + */ + subscribe?: boolean; + /** + * Whether this server supports notifications for changes to the resource list. + */ + listChanged?: boolean; + }; + /** + * Present if the server offers any tools to call. + * + * @example Tools — minimum baseline support + * {@includeCode ./examples/ServerCapabilities/tools-minimum-baseline-support.json} + * + * @example Tools — list changed notifications + * {@includeCode ./examples/ServerCapabilities/tools-list-changed-notifications.json} + */ + tools?: { + /** + * Whether this server supports notifications for changes to the tool list. + */ + listChanged?: boolean; + }; + /** + * Optional MCP extensions that the server supports. Keys are extension identifiers + * (e.g., "io.modelcontextprotocol/tasks"), and values are per-extension settings + * objects. An empty object indicates support with no settings. + * + * Keys MUST follow the {@link MetaObject | `_meta` key naming rules}, with a + * mandatory prefix. + * + * @example Extensions — Tasks extension support + * {@includeCode ./examples/ServerCapabilities/extensions-tasks.json} + */ + extensions?: { [key: string]: JSONObject }; +} + +/** + * An optionally-sized icon that can be displayed in a user interface. + * + * @category Common Types + */ +export interface Icon { + /** + * A standard URI pointing to an icon resource. May be an HTTP/HTTPS URL or a + * `data:` URI with Base64-encoded image data. + * + * Consumers SHOULD take steps to ensure URLs serving icons are from the + * same domain as the client/server or a trusted domain. + * + * Consumers SHOULD take appropriate precautions when consuming SVGs as they can contain + * executable JavaScript. + * + * @format uri + */ + src: string; + + /** + * Optional MIME type override if the source MIME type is missing or generic. + * For example: `"image/png"`, `"image/jpeg"`, or `"image/svg+xml"`. + */ + mimeType?: string; + + /** + * Optional array of strings that specify sizes at which the icon can be used. + * Each string should be in WxH format (e.g., `"48x48"`, `"96x96"`) or `"any"` for scalable formats like SVG. + * + * If not provided, the client should assume that the icon can be used at any size. + */ + sizes?: string[]; + + /** + * Optional specifier for the theme this icon is designed for. `"light"` indicates + * the icon is designed to be used with a light background, and `"dark"` indicates + * the icon is designed to be used with a dark background. + * + * If not provided, the client should assume the icon can be used with any theme. + */ + theme?: "light" | "dark"; +} + +/** + * Base interface to add `icons` property. + * + * @internal + */ +export interface Icons { + /** + * Optional set of sized icons that the client can display in a user interface. + * + * Clients that support rendering icons MUST support at least the following MIME types: + * - `image/png` - PNG images (safe, universal compatibility) + * - `image/jpeg` (and `image/jpg`) - JPEG images (safe, universal compatibility) + * + * Clients that support rendering icons SHOULD also support: + * - `image/svg+xml` - SVG images (scalable but requires security precautions) + * - `image/webp` - WebP images (modern, efficient format) + */ + icons?: Icon[]; +} + +/** + * Base interface for metadata with name (identifier) and title (display name) properties. + * + * @internal + */ +export interface BaseMetadata { + /** + * Intended for programmatic or logical use, but used as a display name in past specs or fallback (if title isn't present). + */ + name: string; + + /** + * Intended for UI and end-user contexts — optimized to be human-readable and easily understood, + * even by those unfamiliar with domain-specific terminology. + * + * If not provided, the name should be used for display (except for {@link Tool}, + * where `annotations.title` should be given precedence over using `name`, + * if present). + */ + title?: string; +} + +/** + * Describes the MCP implementation. + * + * @category `server/discover` + */ +export interface Implementation extends BaseMetadata, Icons { + /** + * The version of this implementation. + */ + version: string; + + /** + * An optional human-readable description of what this implementation does. + * + * This can be used by clients or servers to provide context about their purpose + * and capabilities. For example, a server might describe the types of resources + * or tools it provides, while a client might describe its intended use case. + */ + description?: string; + + /** + * An optional URL of the website for this implementation. + * + * @format uri + */ + websiteUrl?: string; +} + +/* Progress notifications */ + +/** + * Parameters for a {@link ProgressNotification | notifications/progress} notification. + * + * @example Progress message + * {@includeCode ./examples/ProgressNotificationParams/progress-message.json} + * + * @category `notifications/progress` + */ +export interface ProgressNotificationParams extends NotificationParams { + /** + * The progress token which was given in the initial request, used to associate this notification with the request that is proceeding. + */ + progressToken: ProgressToken; + /** + * The progress thus far. This should increase every time progress is made, even if the total is unknown. + * + * @TJS-type number + */ + progress: number; + /** + * Total number of items to process (or total progress required), if known. + * + * @TJS-type number + */ + total?: number; + /** + * An optional message describing the current progress. + */ + message?: string; +} + +/** + * An out-of-band notification used to inform the receiver of a progress update for a long-running request. + * + * @example Progress message + * {@includeCode ./examples/ProgressNotification/progress-message.json} + * + * @category `notifications/progress` + */ +export interface ProgressNotification extends JSONRPCNotification { + method: "notifications/progress"; + params: ProgressNotificationParams; +} + +/* Pagination */ +/** + * Common params for paginated requests. + * + * @example List request with cursor + * {@includeCode ./examples/PaginatedRequestParams/list-with-cursor.json} + * + * @category Common Types + */ +export interface PaginatedRequestParams extends RequestParams { + /** + * An opaque token representing the current pagination position. + * If provided, the server should return results starting after this cursor. + */ + cursor?: Cursor; +} + +/** @internal */ +export interface PaginatedRequest extends JSONRPCRequest { + params: PaginatedRequestParams; +} + +/** @internal */ +export interface PaginatedResult extends Result { + /** + * An opaque token representing the pagination position after the last returned result. + * If present, there may be more results available. + */ + nextCursor?: Cursor; +} + +/** + * A result that supports a time-to-live (TTL) hint for client-side caching. + * + * @internal + */ +export interface CacheableResult extends Result { + /** + * A hint from the server indicating how long (in milliseconds) the + * client MAY cache this response before re-fetching. Semantics are + * analogous to HTTP Cache-Control max-age. + * + * - If 0, The response SHOULD be considered immediately stale, + * The client MAY re-fetch every time the result is needed. + * - If positive, the client SHOULD consider the result fresh for this many + * milliseconds after receiving the response. + * + * @minimum 0 + */ + ttlMs: number; + + /** + * Indicates the intended scope of the cached response, analogous to HTTP + * `Cache-Control: public` vs `Cache-Control: private`. + * + * - `"public"`: The response does not contain user-specific data. Any + * client or intermediary (e.g., shared gateway, caching proxy) MAY cache + * the response and serve it across authorization contexts. + * - `"private"`: The response MAY be cached and reused only within the + * same authorization context. Caches MUST NOT be shared across + * authorization contexts (e.g., a different access token requires a + * different cache). + * + */ + cacheScope: "public" | "private"; +} + +/* Resources */ +/** + * Sent from the client to request a list of resources the server has. + * + * @example List resources request + * {@includeCode ./examples/ListResourcesRequest/list-resources-request.json} + * + * @category `resources/list` + */ +export interface ListResourcesRequest extends PaginatedRequest { + method: "resources/list"; +} + +/** + * The result returned by the server for a {@link ListResourcesRequest | resources/list} request. + * + * @example Resources list with cursor and TTL + * {@includeCode ./examples/ListResourcesResult/resources-list-with-cursor-and-ttl.json} + * + * @category `resources/list` + */ +export interface ListResourcesResult extends PaginatedResult, CacheableResult { + resources: Resource[]; +} + +/** + * A successful response from the server for a {@link ListResourcesRequest | resources/list} request. + * + * @example List resources result response + * {@includeCode ./examples/ListResourcesResultResponse/list-resources-result-response.json} + * + * @category `resources/list` + */ +export interface ListResourcesResultResponse extends JSONRPCResultResponse { + result: ListResourcesResult; +} + +/** + * Sent from the client to request a list of resource templates the server has. + * + * @example List resource templates request + * {@includeCode ./examples/ListResourceTemplatesRequest/list-resource-templates-request.json} + * + * @category `resources/templates/list` + */ +export interface ListResourceTemplatesRequest extends PaginatedRequest { + method: "resources/templates/list"; +} + +/** + * The result returned by the server for a {@link ListResourceTemplatesRequest | resources/templates/list} request. + * + * @example Resource templates list with cursor and TTL + * {@includeCode ./examples/ListResourceTemplatesResult/resource-templates-list-with-cursor-and-ttl.json} + * + * @category `resources/templates/list` + */ +export interface ListResourceTemplatesResult + extends PaginatedResult, CacheableResult { + resourceTemplates: ResourceTemplate[]; +} + +/** + * A successful response from the server for a {@link ListResourceTemplatesRequest | resources/templates/list} request. + * + * @example List resource templates result response + * {@includeCode ./examples/ListResourceTemplatesResultResponse/list-resource-templates-result-response.json} + * + * @category `resources/templates/list` + */ +export interface ListResourceTemplatesResultResponse extends JSONRPCResultResponse { + result: ListResourceTemplatesResult; +} + +/** + * Common params for resource-related requests. + * + * @internal + */ +export interface ResourceRequestParams extends RequestParams { + /** + * The URI of the resource. The URI can use any protocol; it is up to the server how to interpret it. + * + * @format uri + */ + uri: string; +} + +/** + * Parameters for a `resources/read` request. + * + * @category `resources/read` + */ +export interface ReadResourceRequestParams + extends ResourceRequestParams, InputResponseRequestParams {} + +/** + * Sent from the client to the server, to read a specific resource URI. + * + * @example Read resource request + * {@includeCode ./examples/ReadResourceRequest/read-resource-request.json} + * + * @category `resources/read` + */ +export interface ReadResourceRequest extends JSONRPCRequest { + method: "resources/read"; + params: ReadResourceRequestParams; +} + +/** + * The result returned by the server for a {@link ReadResourceRequest | resources/read} request. + * + * @example File resource contents + * {@includeCode ./examples/ReadResourceResult/file-resource-contents.json} + * + * @category `resources/read` + */ +export interface ReadResourceResult extends CacheableResult { + contents: (TextResourceContents | BlobResourceContents)[]; +} + +/** + * A successful response from the server for a {@link ReadResourceRequest | resources/read} request. + * + * @example Read resource result response + * {@includeCode ./examples/ReadResourceResultResponse/read-resource-result-response.json} + * + * @example Read resource result response with TTL + * {@includeCode ./examples/ReadResourceResultResponse/read-resource-result-response-with-ttl.json} + * + * @category `resources/read` + */ +export interface ReadResourceResultResponse extends JSONRPCResultResponse { + result: ReadResourceResult | InputRequiredResult; +} + +/** + * An optional notification from the server to the client, informing it that the list of resources it can read from has changed. This is only delivered on a {@link SubscriptionsListenRequest | subscriptions/listen} stream when the client requested it via the `resourcesListChanged` filter field. + * + * @example Resources list changed + * {@includeCode ./examples/ResourceListChangedNotification/resources-list-changed.json} + * + * @category `notifications/resources/list_changed` + */ +export interface ResourceListChangedNotification extends JSONRPCNotification { + method: "notifications/resources/list_changed"; + params?: NotificationParams; +} + +/** + * The set of notification types a client may opt in to on a + * {@link SubscriptionsListenRequest | subscriptions/listen} request. + * + * Each notification type is **opt-in**; the server **MUST NOT** send + * notification types the client has not explicitly requested here. + * + * @category `subscriptions/listen` + */ +export interface SubscriptionFilter { + /** + * If true, receive {@link ToolListChangedNotification | notifications/tools/list_changed}. + */ + toolsListChanged?: boolean; + /** + * If true, receive {@link PromptListChangedNotification | notifications/prompts/list_changed}. + */ + promptsListChanged?: boolean; + /** + * If true, receive {@link ResourceListChangedNotification | notifications/resources/list_changed}. + */ + resourcesListChanged?: boolean; + /** + * Subscribe to {@link ResourceUpdatedNotification | notifications/resources/updated} for these resource URIs. + * Replaces the former `resources/subscribe` RPC. + */ + resourceSubscriptions?: string[]; +} + +/** + * Parameters for a {@link SubscriptionsListenRequest | subscriptions/listen} request. + * + * @category `subscriptions/listen` + */ +export interface SubscriptionsListenRequestParams extends RequestParams { + /** + * The notifications the client opts in to on this stream. The server + * **MUST NOT** send notification types the client has not explicitly + * requested. + */ + notifications: SubscriptionFilter; +} + +/** + * Sent from the client to open a long-lived channel for receiving notifications + * outside the context of a specific request. Replaces the previous HTTP GET + * endpoint and ensures consistent behavior between HTTP and STDIO. + * + * @example Listen for tools and resource list changes + * {@includeCode ./examples/SubscriptionsListenRequest/listen-for-list-changes.json} + * + * @category `subscriptions/listen` + */ +export interface SubscriptionsListenRequest extends JSONRPCRequest { + method: "subscriptions/listen"; + params: SubscriptionsListenRequestParams; +} + +/** + * Extends {@link ResultMetaObject} with the subscription-stream identifier carried by a + * {@link SubscriptionsListenResult}. All key naming rules from `MetaObject` apply. + * + * @see {@link MetaObject} for key naming rules and reserved prefixes. + * @category `subscriptions/listen` + */ +export interface SubscriptionsListenResultMetaObject extends ResultMetaObject { + /** + * Identifies the subscription stream this response closes, so the client can + * correlate it with the originating subscription — mirroring the same key on + * the stream's notifications. The value is the JSON-RPC ID of the + * `subscriptions/listen` request that opened the stream (and equals this + * response's `id`). + */ + "io.modelcontextprotocol/subscriptionId": RequestId; +} + +/** + * The response to a {@link SubscriptionsListenRequest | subscriptions/listen} + * request, signalling that the subscription has ended gracefully (for example, + * during server shutdown). Because the listen stream is long-lived, this result + * is sent only when the server tears the subscription down; an abrupt transport + * close carries no response. The result body is otherwise empty. + * + * @example Subscription closed gracefully + * {@includeCode ./examples/SubscriptionsListenResult/listen-closed.json} + * + * @category `subscriptions/listen` + */ +export interface SubscriptionsListenResult extends Result { + _meta: SubscriptionsListenResultMetaObject; +} + +/** + * A successful response from the server for a {@link SubscriptionsListenRequest | subscriptions/listen} + * request, sent when the server tears the subscription down gracefully. + * + * @example Subscription closed gracefully response + * {@includeCode ./examples/SubscriptionsListenResultResponse/listen-closed-response.json} + * + * @category `subscriptions/listen` + */ +export interface SubscriptionsListenResultResponse extends JSONRPCResultResponse { + result: SubscriptionsListenResult; +} + +/** + * Parameters for a {@link SubscriptionsAcknowledgedNotification | notifications/subscriptions/acknowledged} notification. + * + * @category `notifications/subscriptions/acknowledged` + */ +export interface SubscriptionsAcknowledgedNotificationParams extends NotificationParams { + /** + * The subset of requested notification types the server agreed to honor. + * Only includes notification types the server actually supports; if the + * client requested an unsupported type (e.g., `promptsListChanged` when + * the server has no prompts), it is omitted from this set. + */ + notifications: SubscriptionFilter; +} + +/** + * Sent by the server to acknowledge that a + * {@link SubscriptionsListenRequest | subscriptions/listen} subscription has been + * established and to report which notification types it agreed to honor. + * + * This notification MUST be the first message the server sends carrying the + * subscription's ID in `io.modelcontextprotocol/subscriptionId`. The server MUST + * NOT send any notification on the subscription before acknowledging it. On + * stdio, where every subscription shares one channel, this ordering is defined + * per subscription ID and not per channel: messages belonging to other + * subscriptions MAY be interleaved before it. + * + * @example Listen acknowledged + * {@includeCode ./examples/SubscriptionsAcknowledgedNotification/listen-acknowledged.json} + * + * @category `notifications/subscriptions/acknowledged` + */ +export interface SubscriptionsAcknowledgedNotification extends JSONRPCNotification { + method: "notifications/subscriptions/acknowledged"; + params: SubscriptionsAcknowledgedNotificationParams; +} + +/** + * Parameters for a `notifications/resources/updated` notification. + * + * @example File resource updated + * {@includeCode ./examples/ResourceUpdatedNotificationParams/file-resource-updated.json} + * + * @category `notifications/resources/updated` + */ +export interface ResourceUpdatedNotificationParams extends NotificationParams { + /** + * The URI of the resource that has been updated. This might be a sub-resource of the one that the client actually subscribed to. + * + * @format uri + */ + uri: string; +} + +/** + * A notification from the server to the client, informing it that a resource has changed and may need to be read again. This is only sent for resources the client opted in to via the `resourceSubscriptions` field of a {@link SubscriptionsListenRequest | subscriptions/listen} request. + * + * @example File resource updated notification + * {@includeCode ./examples/ResourceUpdatedNotification/file-resource-updated-notification.json} + * + * @category `notifications/resources/updated` + */ +export interface ResourceUpdatedNotification extends JSONRPCNotification { + method: "notifications/resources/updated"; + params: ResourceUpdatedNotificationParams; +} + +/** + * A known resource that the server is capable of reading. + * + * @example File resource with annotations + * {@includeCode ./examples/Resource/file-resource-with-annotations.json} + * + * @category `resources/list` + */ +export interface Resource extends BaseMetadata, Icons { + /** + * The URI of this resource. + * + * @format uri + */ + uri: string; + + /** + * A description of what this resource represents. + * + * This can be used by clients to improve the LLM's understanding of available resources. It can be thought of like a "hint" to the model. + */ + description?: string; + + /** + * The MIME type of this resource, if known. + */ + mimeType?: string; + + /** + * Optional annotations for the client. + */ + annotations?: Annotations; + + /** + * The size of the raw resource content, in bytes (i.e., before base64 encoding or any tokenization), if known. + * + * This can be used by Hosts to display file sizes and estimate context window usage. + */ + size?: number; + + _meta?: MetaObject; +} + +/** + * A template description for resources available on the server. + * + * @category `resources/templates/list` + */ +export interface ResourceTemplate extends BaseMetadata, Icons { + /** + * A URI template (according to RFC 6570) that can be used to construct resource URIs. + * + * @format uri-template + */ + uriTemplate: string; + + /** + * A description of what this template is for. + * + * This can be used by clients to improve the LLM's understanding of available resources. It can be thought of like a "hint" to the model. + */ + description?: string; + + /** + * The MIME type for all resources that match this template. This should only be included if all resources matching this template have the same type. + */ + mimeType?: string; + + /** + * Optional annotations for the client. + */ + annotations?: Annotations; + + _meta?: MetaObject; +} + +/** + * The contents of a specific resource or sub-resource. + * + * @internal + */ +export interface ResourceContents { + /** + * The URI of this resource. + * + * @format uri + */ + uri: string; + /** + * The MIME type of this resource, if known. + */ + mimeType?: string; + + _meta?: MetaObject; +} + +/** + * @example Text file contents + * {@includeCode ./examples/TextResourceContents/text-file-contents.json} + * + * @category Content + */ +export interface TextResourceContents extends ResourceContents { + /** + * The text of the item. This must only be set if the item can actually be represented as text (not binary data). + */ + text: string; +} + +/** + * @example Image file contents + * {@includeCode ./examples/BlobResourceContents/image-file-contents.json} + * + * @category Content + */ +export interface BlobResourceContents extends ResourceContents { + /** + * A base64-encoded string representing the binary data of the item. + * + * @format byte + */ + blob: string; +} + +/* Prompts */ +/** + * Sent from the client to request a list of prompts and prompt templates the server has. + * + * @example List prompts request + * {@includeCode ./examples/ListPromptsRequest/list-prompts-request.json} + * + * @category `prompts/list` + */ +export interface ListPromptsRequest extends PaginatedRequest { + method: "prompts/list"; +} + +/** + * The result returned by the server for a {@link ListPromptsRequest | prompts/list} request. + * + * @example Prompts list with cursor and TTL + * {@includeCode ./examples/ListPromptsResult/prompts-list-with-cursor-and-ttl.json} + * + * @category `prompts/list` + */ +export interface ListPromptsResult extends PaginatedResult, CacheableResult { + prompts: Prompt[]; +} + +/** + * A successful response from the server for a {@link ListPromptsRequest | prompts/list} request. + * + * @example List prompts result response + * {@includeCode ./examples/ListPromptsResultResponse/list-prompts-result-response.json} + * + * @category `prompts/list` + */ +export interface ListPromptsResultResponse extends JSONRPCResultResponse { + result: ListPromptsResult; +} + +/** + * Parameters for a `prompts/get` request. + * + * @example Get code review prompt + * {@includeCode ./examples/GetPromptRequestParams/get-code-review-prompt.json} + * + * @category `prompts/get` + */ +export interface GetPromptRequestParams extends InputResponseRequestParams { + /** + * The name of the prompt or prompt template. + */ + name: string; + /** + * Arguments to use for templating the prompt. + */ + arguments?: { [key: string]: string }; +} + +/** + * Used by the client to get a prompt provided by the server. + * + * @example Get prompt request + * {@includeCode ./examples/GetPromptRequest/get-prompt-request.json} + * + * @category `prompts/get` + */ +export interface GetPromptRequest extends JSONRPCRequest { + method: "prompts/get"; + params: GetPromptRequestParams; +} + +/** + * The result returned by the server for a {@link GetPromptRequest | prompts/get} request. + * + * @example Code review prompt + * {@includeCode ./examples/GetPromptResult/code-review-prompt.json} + * + * @category `prompts/get` + */ +export interface GetPromptResult extends Result { + /** + * An optional description for the prompt. + */ + description?: string; + messages: PromptMessage[]; +} + +/** + * A successful response from the server for a {@link GetPromptRequest | prompts/get} request. + * + * @example Get prompt result response + * {@includeCode ./examples/GetPromptResultResponse/get-prompt-result-response.json} + * + * @category `prompts/get` + */ +export interface GetPromptResultResponse extends JSONRPCResultResponse { + result: GetPromptResult | InputRequiredResult; +} + +/** + * A prompt or prompt template that the server offers. + * + * @category `prompts/list` + */ +export interface Prompt extends BaseMetadata, Icons { + /** + * An optional description of what this prompt provides + */ + description?: string; + + /** + * A list of arguments to use for templating the prompt. + */ + arguments?: PromptArgument[]; + + _meta?: MetaObject; +} + +/** + * Describes an argument that a prompt can accept. + * + * @category `prompts/list` + */ +export interface PromptArgument extends BaseMetadata { + /** + * A human-readable description of the argument. + */ + description?: string; + /** + * Whether this argument must be provided. + */ + required?: boolean; +} + +/** + * The sender or recipient of messages and data in a conversation. + * + * @category Common Types + */ +export type Role = "user" | "assistant"; + +/** + * Describes a message returned as part of a prompt. + * + * This is similar to {@link SamplingMessage}, but also supports the embedding of + * resources from the MCP server. + * + * @category `prompts/get` + */ +export interface PromptMessage { + role: Role; + content: ContentBlock; +} + +/** + * A resource that the server is capable of reading, included in a prompt or tool call result. + * + * Note: resource links returned by tools are not guaranteed to appear in the results of {@link ListResourcesRequest | resources/list} requests. + * + * @example File resource link + * {@includeCode ./examples/ResourceLink/file-resource-link.json} + * + * @category Content + */ +export interface ResourceLink extends Resource { + type: "resource_link"; +} + +/** + * The contents of a resource, embedded into a prompt or tool call result. + * + * It is up to the client how best to render embedded resources for the benefit + * of the LLM and/or the user. + * + * @example Embedded file resource with annotations + * {@includeCode ./examples/EmbeddedResource/embedded-file-resource-with-annotations.json} + * + * @category Content + */ +export interface EmbeddedResource { + type: "resource"; + resource: TextResourceContents | BlobResourceContents; + + /** + * Optional annotations for the client. + */ + annotations?: Annotations; + + _meta?: MetaObject; +} +/** + * An optional notification from the server to the client, informing it that the list of prompts it offers has changed. This is only delivered on a {@link SubscriptionsListenRequest | subscriptions/listen} stream when the client requested it via the `promptsListChanged` filter field. + * + * @example Prompts list changed + * {@includeCode ./examples/PromptListChangedNotification/prompts-list-changed.json} + * + * @category `notifications/prompts/list_changed` + */ +export interface PromptListChangedNotification extends JSONRPCNotification { + method: "notifications/prompts/list_changed"; + params?: NotificationParams; +} + +/* Tools */ +/** + * Sent from the client to request a list of tools the server has. + * + * @example List tools request + * {@includeCode ./examples/ListToolsRequest/list-tools-request.json} + * + * @category `tools/list` + */ +export interface ListToolsRequest extends PaginatedRequest { + method: "tools/list"; +} + +/** + * The result returned by the server for a {@link ListToolsRequest | tools/list} request. + * + * @example Tools list with cursor and TTL + * {@includeCode ./examples/ListToolsResult/tools-list-with-cursor-and-ttl.json} + * + * @category `tools/list` + */ +export interface ListToolsResult extends PaginatedResult, CacheableResult { + tools: Tool[]; +} + +/** + * A successful response from the server for a {@link ListToolsRequest | tools/list} request. + * + * @example List tools result response + * {@includeCode ./examples/ListToolsResultResponse/list-tools-result-response.json} + * + * @category `tools/list` + */ +export interface ListToolsResultResponse extends JSONRPCResultResponse { + result: ListToolsResult; +} + +/** + * The result returned by the server for a {@link CallToolRequest | tools/call} request. + * + * @example Result with unstructured text + * {@includeCode ./examples/CallToolResult/result-with-unstructured-text.json} + * + * @example Result with structured content + * {@includeCode ./examples/CallToolResult/result-with-structured-content.json} + * + * @example Invalid tool input error + * {@includeCode ./examples/CallToolResult/invalid-tool-input-error.json} + * + * @category `tools/call` + */ +export interface CallToolResult extends Result { + /** + * A list of content objects that represent the unstructured result of the tool call. + */ + content: ContentBlock[]; + + /** + * An optional JSON value that represents the structured result of the tool call. + * + * This can be any JSON value (object, array, string, number, boolean, or null) + * that conforms to the tool's outputSchema if one is defined. + */ + structuredContent?: unknown; + + /** + * Whether the tool call ended in an error. + * + * If not set, this is assumed to be false (the call was successful). + * + * Any errors that originate from the tool SHOULD be reported inside the result + * object, with `isError` set to true, _not_ as an MCP protocol-level error + * response. Otherwise, the LLM would not be able to see that an error occurred + * and self-correct. + * + * However, any errors in _finding_ the tool, an error indicating that the + * server does not support tool calls, or any other exceptional conditions, + * should be reported as an MCP error response. + */ + isError?: boolean; +} + +/** + * A successful response from the server for a {@link CallToolRequest | tools/call} request. + * + * @example Call tool result response + * {@includeCode ./examples/CallToolResultResponse/call-tool-result-response.json} + * + * @category `tools/call` + */ +export interface CallToolResultResponse extends JSONRPCResultResponse { + result: CallToolResult | InputRequiredResult; +} + +/** + * Parameters for a `tools/call` request. + * + * @example `get_weather` tool call params + * {@includeCode ./examples/CallToolRequestParams/get-weather-tool-call-params.json} + * + * @example Tool call params with progress token + * {@includeCode ./examples/CallToolRequestParams/tool-call-params-with-progress-token.json} + * + * @category `tools/call` + */ +export interface CallToolRequestParams extends InputResponseRequestParams { + /** + * The name of the tool. + */ + name: string; + /** + * Arguments to use for the tool call. + */ + arguments?: { [key: string]: unknown }; +} + +/** + * Used by the client to invoke a tool provided by the server. + * + * @example Call tool request + * {@includeCode ./examples/CallToolRequest/call-tool-request.json} + * + * @category `tools/call` + */ +export interface CallToolRequest extends JSONRPCRequest { + method: "tools/call"; + params: CallToolRequestParams; +} + +/** + * An optional notification from the server to the client, informing it that the list of tools it offers has changed. This is only delivered on a {@link SubscriptionsListenRequest | subscriptions/listen} stream when the client requested it via the `toolsListChanged` filter field. + * + * @example Tools list changed + * {@includeCode ./examples/ToolListChangedNotification/tools-list-changed.json} + * + * @category `notifications/tools/list_changed` + */ +export interface ToolListChangedNotification extends JSONRPCNotification { + method: "notifications/tools/list_changed"; + params?: NotificationParams; +} + +/** + * Additional properties describing a {@link Tool} to clients. + * + * NOTE: all properties in `ToolAnnotations` are **hints**. + * They are not guaranteed to provide a faithful description of + * tool behavior (including descriptive properties like `title`). + * + * Clients should never make tool use decisions based on `ToolAnnotations` + * received from untrusted servers. + * + * @category `tools/list` + */ +export interface ToolAnnotations { + /** + * A human-readable title for the tool. + */ + title?: string; + + /** + * If true, the tool does not modify its environment. + * + * Default: false + */ + readOnlyHint?: boolean; + + /** + * If true, the tool may perform destructive updates to its environment. + * If false, the tool performs only additive updates. + * + * (This property is meaningful only when `readOnlyHint == false`) + * + * Default: true + */ + destructiveHint?: boolean; + + /** + * If true, calling the tool repeatedly with the same arguments + * will have no additional effect on its environment. + * + * (This property is meaningful only when `readOnlyHint == false`) + * + * Default: false + */ + idempotentHint?: boolean; + + /** + * If true, this tool may interact with an "open world" of external + * entities. If false, the tool's domain of interaction is closed. + * For example, the world of a web search tool is open, whereas that + * of a memory tool is not. + * + * Default: true + */ + openWorldHint?: boolean; +} + +/** + * Definition for a tool the client can call. + * + * @example With default 2020-12 input schema + * {@includeCode ./examples/Tool/with-default-2020-12-input-schema.json} + * + * @example With explicit draft-07 input schema + * {@includeCode ./examples/Tool/with-explicit-draft-07-input-schema.json} + * + * @example With no parameters + * {@includeCode ./examples/Tool/with-no-parameters.json} + * + * @example With output schema for structured content + * {@includeCode ./examples/Tool/with-output-schema-for-structured-content.json} + * + * @category `tools/list` + */ +export interface Tool extends BaseMetadata, Icons { + /** + * A human-readable description of the tool. + * + * This can be used by clients to improve the LLM's understanding of available tools. It can be thought of like a "hint" to the model. + */ + description?: string; + + /** + * A JSON Schema object defining the expected parameters for the tool. + * + * Tool arguments are always JSON objects, so `type: "object"` is required at the root. + * Beyond that, any JSON Schema 2020-12 keyword may appear alongside `type` — including + * composition keywords (`oneOf`, `anyOf`, `allOf`, `not`), conditional keywords + * (`if`/`then`/`else`), reference keywords (`$ref`, `$defs`, `$anchor`), and any other + * standard validation or annotation keywords. + * + * Property schemas may carry an `x-mcp-header` annotation to mirror the + * argument value into an HTTP header on the Streamable HTTP transport. See + * the Streamable HTTP transport specification for the validity and + * extraction rules. + * + * Defaults to JSON Schema 2020-12 when no explicit `$schema` is provided. + */ + inputSchema: { $schema?: string; type: "object"; [key: string]: unknown }; + + /** + * An optional JSON Schema object defining the structure of the tool's output returned in + * the structuredContent field of a {@link CallToolResult}. This can be any valid JSON Schema 2020-12. + * + * Defaults to JSON Schema 2020-12 when no explicit `$schema` is provided. + */ + outputSchema?: { $schema?: string; [key: string]: unknown }; + + /** + * Optional additional tool information. + * + * Display name precedence order is: `title`, `annotations.title`, then `name`. + */ + annotations?: ToolAnnotations; + + _meta?: MetaObject; +} + +/* Logging */ + +/** + * Parameters for a `notifications/message` notification. + * + * @deprecated Deprecated as of protocol version 2026-07-28 (SEP-2577). + * Remains in the specification for at least twelve months; see the + * deprecated features registry. + * + * @example Log database connection failed + * {@includeCode ./examples/LoggingMessageNotificationParams/log-database-connection-failed.json} + * + * @category `notifications/message` + */ +export interface LoggingMessageNotificationParams extends NotificationParams { + /** + * The severity of this log message. + */ + level: LoggingLevel; + /** + * An optional name of the logger issuing this message. + */ + logger?: string; + /** + * The data to be logged, such as a string message or an object. Any JSON serializable type is allowed here. + */ + data: unknown; +} + +/** + * JSONRPCNotification of a log message passed from server to client. The client opts in by setting `"io.modelcontextprotocol/logLevel"` in a request's `_meta`. + * + * @deprecated Deprecated as of protocol version 2026-07-28 (SEP-2577). + * Remains in the specification for at least twelve months; see the + * deprecated features registry. + * + * @example Log database connection failed + * {@includeCode ./examples/LoggingMessageNotification/log-database-connection-failed.json} + * + * @category `notifications/message` + */ +export interface LoggingMessageNotification extends JSONRPCNotification { + method: "notifications/message"; + params: LoggingMessageNotificationParams; +} + +/** + * The severity of a log message. + * + * These map to syslog message severities, as specified in RFC-5424: + * https://datatracker.ietf.org/doc/html/rfc5424#section-6.2.1 + * + * @deprecated Deprecated as of protocol version 2026-07-28 (SEP-2577). + * Remains in the specification for at least twelve months; see the + * deprecated features registry. + * + * @category Common Types + */ +export type LoggingLevel = + | "debug" + | "info" + | "notice" + | "warning" + | "error" + | "critical" + | "alert" + | "emergency"; + +/* Sampling */ +/** + * Parameters for a `sampling/createMessage` request. + * + * @example Basic request + * {@includeCode ./examples/CreateMessageRequestParams/basic-request.json} + * + * @example Request with tools + * {@includeCode ./examples/CreateMessageRequestParams/request-with-tools.json} + * + * @example Follow-up request with tool results + * {@includeCode ./examples/CreateMessageRequestParams/follow-up-with-tool-results.json} + * + * @deprecated Deprecated as of protocol version 2026-07-28 (SEP-2577). + * Remains in the specification for at least twelve months; see the + * deprecated features registry. + * + * @category `sampling/createMessage` + */ +export interface CreateMessageRequestParams { + messages: SamplingMessage[]; + /** + * The server's preferences for which model to select. The client MAY ignore these preferences. + */ + modelPreferences?: ModelPreferences; + /** + * An optional system prompt the server wants to use for sampling. The client MAY modify or omit this prompt. + */ + systemPrompt?: string; + /** + * A request to include context from one or more MCP servers (including the caller), to be attached to the prompt. + * The client MAY ignore this request. + * + * Default is `"none"`. The values `"thisServer"` and `"allServers"` are deprecated (SEP-2596): servers SHOULD + * omit this field or use `"none"`, and SHOULD only use the deprecated values if the client declares + * {@link ClientCapabilities.sampling.context}. + * + * @deprecated The `"thisServer"` and `"allServers"` values are deprecated as of protocol version 2025-11-25 + * (SEP-2596) and will be removed no later than the Sampling feature itself (SEP-2577). Omit this field or use `"none"`. + */ + includeContext?: "none" | "thisServer" | "allServers"; + /** + * @TJS-type number + */ + temperature?: number; + /** + * The requested maximum number of tokens to sample (to prevent runaway completions). + * + * The client MAY choose to sample fewer tokens than the requested maximum. + */ + maxTokens: number; + stopSequences?: string[]; + /** + * Optional metadata to pass through to the LLM provider. The format of this metadata is provider-specific. + */ + metadata?: JSONObject; + /** + * Tools that the model may use during generation. + * The client MUST return an error if this field is provided but {@link ClientCapabilities.sampling.tools} is not declared. + */ + tools?: Tool[]; + /** + * Controls how the model uses tools. + * The client MUST return an error if this field is provided but {@link ClientCapabilities.sampling.tools} is not declared. + * Default is `{ mode: "auto" }`. + */ + toolChoice?: ToolChoice; +} + +/** + * Controls tool selection behavior for sampling requests. + * + * @deprecated Deprecated as of protocol version 2026-07-28 (SEP-2577). + * Remains in the specification for at least twelve months; see the + * deprecated features registry. + * + * @category `sampling/createMessage` + */ +export interface ToolChoice { + /** + * Controls the tool use ability of the model: + * - `"auto"`: Model decides whether to use tools (default) + * - `"required"`: Model MUST use at least one tool before completing + * - `"none"`: Model MUST NOT use any tools + */ + mode?: "auto" | "required" | "none"; +} + +/** + * A request from the server to sample an LLM via the client. The client has full discretion over which model to select. The client should also inform the user before beginning sampling, to allow them to inspect the request (human in the loop) and decide whether to approve it. + * + * @example Sampling request + * {@includeCode ./examples/CreateMessageRequest/sampling-request.json} + * + * @deprecated Deprecated as of protocol version 2026-07-28 (SEP-2577). + * Remains in the specification for at least twelve months; see the + * deprecated features registry. + * + * @category `sampling/createMessage` + */ +export interface CreateMessageRequest { + method: "sampling/createMessage"; + params: CreateMessageRequestParams; +} + +/** + * The result returned by the client for a {@link CreateMessageRequest | sampling/createMessage} request. + * The client should inform the user before returning the sampled message, to allow them + * to inspect the response (human in the loop) and decide whether to allow the server to see it. + * + * @example Text response + * {@includeCode ./examples/CreateMessageResult/text-response.json} + * + * @example Tool use response + * {@includeCode ./examples/CreateMessageResult/tool-use-response.json} + * + * @example Final response after tool use + * {@includeCode ./examples/CreateMessageResult/final-response.json} + * + * @deprecated Deprecated as of protocol version 2026-07-28 (SEP-2577). + * Remains in the specification for at least twelve months; see the + * deprecated features registry. + * + * @category `sampling/createMessage` + */ +export interface CreateMessageResult extends SamplingMessage { + /** + * The name of the model that generated the message. + */ + model: string; + + /** + * The reason why sampling stopped, if known. + * + * Standard values: + * - `"endTurn"`: Natural end of the assistant's turn + * - `"stopSequence"`: A stop sequence was encountered + * - `"maxTokens"`: Maximum token limit was reached + * - `"toolUse"`: The model wants to use one or more tools + * + * This field is an open string to allow for provider-specific stop reasons. + */ + stopReason?: "endTurn" | "stopSequence" | "maxTokens" | "toolUse" | string; +} + +/** + * Describes a message issued to or received from an LLM API. + * + * @example Single content block + * {@includeCode ./examples/SamplingMessage/single-content-block.json} + * + * @example Multiple content blocks + * {@includeCode ./examples/SamplingMessage/multiple-content-blocks.json} + * + * @deprecated Deprecated as of protocol version 2026-07-28 (SEP-2577). + * Remains in the specification for at least twelve months; see the + * deprecated features registry. + * + * @category `sampling/createMessage` + */ +export interface SamplingMessage { + role: Role; + content: SamplingMessageContentBlock | SamplingMessageContentBlock[]; + _meta?: MetaObject; +} + +/** + * @deprecated Deprecated as of protocol version 2026-07-28 (SEP-2577). + * Remains in the specification for at least twelve months; see the + * deprecated features registry. + * + * @category `sampling/createMessage` + */ +export type SamplingMessageContentBlock = + | TextContent + | ImageContent + | AudioContent + | ToolUseContent + | ToolResultContent; + +/** + * Optional annotations for the client. The client can use annotations to inform how objects are used or displayed + * + * @category Common Types + */ +export interface Annotations { + /** + * Describes who the intended audience of this object or data is. + * + * It can include multiple entries to indicate content useful for multiple audiences (e.g., `["user", "assistant"]`). + */ + audience?: Role[]; + + /** + * Describes how important this data is for operating the server. + * + * A value of 1 means "most important," and indicates that the data is + * effectively required, while 0 means "least important," and indicates that + * the data is entirely optional. + * + * @TJS-type number + * @minimum 0 + * @maximum 1 + */ + priority?: number; + + /** + * The moment the resource was last modified, as an ISO 8601 formatted string. + * + * Should be an ISO 8601 formatted string (e.g., "2025-01-12T15:00:58Z"). + * + * Examples: last activity timestamp in an open file, timestamp when the resource + * was attached, etc. + */ + lastModified?: string; +} + +/** + * @category Content + */ +export type ContentBlock = + TextContent | ImageContent | AudioContent | ResourceLink | EmbeddedResource; + +/** + * Text provided to or from an LLM. + * + * @example Text content + * {@includeCode ./examples/TextContent/text-content.json} + * + * @category Content + */ +export interface TextContent { + type: "text"; + + /** + * The text content of the message. + */ + text: string; + + /** + * Optional annotations for the client. + */ + annotations?: Annotations; + + _meta?: MetaObject; +} + +/** + * An image provided to or from an LLM. + * + * @example `image/png` content with annotations + * {@includeCode ./examples/ImageContent/image-png-content-with-annotations.json} + * + * @category Content + */ +export interface ImageContent { + type: "image"; + + /** + * The base64-encoded image data. + * + * @format byte + */ + data: string; + + /** + * The MIME type of the image. Different providers may support different image types. + */ + mimeType: string; + + /** + * Optional annotations for the client. + */ + annotations?: Annotations; + + _meta?: MetaObject; +} + +/** + * Audio provided to or from an LLM. + * + * @example `audio/wav` content + * {@includeCode ./examples/AudioContent/audio-wav-content.json} + * + * @category Content + */ +export interface AudioContent { + type: "audio"; + + /** + * The base64-encoded audio data. + * + * @format byte + */ + data: string; + + /** + * The MIME type of the audio. Different providers may support different audio types. + */ + mimeType: string; + + /** + * Optional annotations for the client. + */ + annotations?: Annotations; + + _meta?: MetaObject; +} + +/** + * A request from the assistant to call a tool. + * + * @example `get_weather` tool use + * {@includeCode ./examples/ToolUseContent/get-weather-tool-use.json} + * + * @deprecated Deprecated as of protocol version 2026-07-28 (SEP-2577). + * Remains in the specification for at least twelve months; see the + * deprecated features registry. + * + * @category `sampling/createMessage` + */ +export interface ToolUseContent { + type: "tool_use"; + + /** + * A unique identifier for this tool use. + * + * This ID is used to match tool results to their corresponding tool uses. + */ + id: string; + + /** + * The name of the tool to call. + */ + name: string; + + /** + * The arguments to pass to the tool, conforming to the tool's input schema. + */ + input: { [key: string]: unknown }; + + /** + * Optional metadata about the tool use. Clients SHOULD preserve this field when + * including tool uses in subsequent sampling requests to enable caching optimizations. + */ + _meta?: MetaObject; +} + +/** + * The result of a tool use, provided by the user back to the assistant. + * + * @example `get_weather` tool result + * {@includeCode ./examples/ToolResultContent/get-weather-tool-result.json} + * + * @deprecated Deprecated as of protocol version 2026-07-28 (SEP-2577). + * Remains in the specification for at least twelve months; see the + * deprecated features registry. + * + * @category `sampling/createMessage` + */ +export interface ToolResultContent { + type: "tool_result"; + + /** + * The ID of the tool use this result corresponds to. + * + * This MUST match the ID from a previous {@link ToolUseContent}. + */ + toolUseId: string; + + /** + * The unstructured result content of the tool use. + * + * This has the same format as {@link CallToolResult.content} and can include text, images, + * audio, resource links, and embedded resources. + */ + content: ContentBlock[]; + + /** + * An optional structured result value. + * + * This can be any JSON value (object, array, string, number, boolean, or null). + * If the tool defined an {@link Tool.outputSchema}, this SHOULD conform to that schema. + */ + structuredContent?: unknown; + + /** + * Whether the tool use resulted in an error. + * + * If true, the content typically describes the error that occurred. + * Default: false + */ + isError?: boolean; + + /** + * Optional metadata about the tool result. Clients SHOULD preserve this field when + * including tool results in subsequent sampling requests to enable caching optimizations. + */ + _meta?: MetaObject; +} + +/** + * The server's preferences for model selection, requested of the client during sampling. + * + * Because LLMs can vary along multiple dimensions, choosing the "best" model is + * rarely straightforward. Different models excel in different areas—some are + * faster but less capable, others are more capable but more expensive, and so + * on. This interface allows servers to express their priorities across multiple + * dimensions to help clients make an appropriate selection for their use case. + * + * These preferences are always advisory. The client MAY ignore them. It is also + * up to the client to decide how to interpret these preferences and how to + * balance them against other considerations. + * + * @example With hints and priorities + * {@includeCode ./examples/ModelPreferences/with-hints-and-priorities.json} + * + * @deprecated Deprecated as of protocol version 2026-07-28 (SEP-2577). + * Remains in the specification for at least twelve months; see the + * deprecated features registry. + * + * @category `sampling/createMessage` + */ +export interface ModelPreferences { + /** + * Optional hints to use for model selection. + * + * If multiple hints are specified, the client MUST evaluate them in order + * (such that the first match is taken). + * + * The client SHOULD prioritize these hints over the numeric priorities, but + * MAY still use the priorities to select from ambiguous matches. + */ + hints?: ModelHint[]; + + /** + * How much to prioritize cost when selecting a model. A value of 0 means cost + * is not important, while a value of 1 means cost is the most important + * factor. + * + * @TJS-type number + * @minimum 0 + * @maximum 1 + */ + costPriority?: number; + + /** + * How much to prioritize sampling speed (latency) when selecting a model. A + * value of 0 means speed is not important, while a value of 1 means speed is + * the most important factor. + * + * @TJS-type number + * @minimum 0 + * @maximum 1 + */ + speedPriority?: number; + + /** + * How much to prioritize intelligence and capabilities when selecting a + * model. A value of 0 means intelligence is not important, while a value of 1 + * means intelligence is the most important factor. + * + * @TJS-type number + * @minimum 0 + * @maximum 1 + */ + intelligencePriority?: number; +} + +/** + * Hints to use for model selection. + * + * Keys not declared here are currently left unspecified by the spec and are up + * to the client to interpret. + * + * @deprecated Deprecated as of protocol version 2026-07-28 (SEP-2577). + * Remains in the specification for at least twelve months; see the + * deprecated features registry. + * + * @category `sampling/createMessage` + */ +export interface ModelHint { + /** + * A hint for a model name. + * + * The client SHOULD treat this as a substring of a model name; for example: + * - `claude-3-5-sonnet` should match `claude-3-5-sonnet-20241022` + * - `sonnet` should match `claude-3-5-sonnet-20241022`, `claude-3-sonnet-20240229`, etc. + * - `claude` should match any Claude model + * + * The client MAY also map the string to a different provider's model name or a different model family, as long as it fills a similar niche; for example: + * - `gemini-1.5-flash` could match `claude-3-haiku-20240307` + */ + name?: string; +} + +/* Autocomplete */ +/** + * Parameters for a `completion/complete` request. + * + * @category `completion/complete` + * + * @example Prompt argument completion + * {@includeCode ./examples/CompleteRequestParams/prompt-argument-completion.json} + * + * @example Prompt argument completion with context + * {@includeCode ./examples/CompleteRequestParams/prompt-argument-completion-with-context.json} + */ +export interface CompleteRequestParams extends RequestParams { + ref: PromptReference | ResourceTemplateReference; + /** + * The argument's information + */ + argument: { + /** + * The name of the argument + */ + name: string; + /** + * The value of the argument to use for completion matching. + */ + value: string; + }; + + /** + * Additional, optional context for completions + */ + context?: { + /** + * Previously-resolved variables in a URI template or prompt. + */ + arguments?: { [key: string]: string }; + }; +} + +/** + * A request from the client to the server, to ask for completion options. + * + * @example Completion request + * {@includeCode ./examples/CompleteRequest/completion-request.json} + * + * @category `completion/complete` + */ +export interface CompleteRequest extends JSONRPCRequest { + method: "completion/complete"; + params: CompleteRequestParams; +} + +/** + * The result returned by the server for a {@link CompleteRequest | completion/complete} request. + * + * @category `completion/complete` + * + * @example Single completion value + * {@includeCode ./examples/CompleteResult/single-completion-value.json} + * + * @example Multiple completion values with more available + * {@includeCode ./examples/CompleteResult/multiple-completion-values-with-more-available.json} + */ +export interface CompleteResult extends Result { + completion: { + /** + * An array of completion values. Must not exceed 100 items. + * + * @maxItems 100 + */ + values: string[]; + /** + * The total number of completion options available. This can exceed the number of values actually sent in the response. + */ + total?: number; + /** + * Indicates whether there are additional completion options beyond those provided in the current response, even if the exact total is unknown. + */ + hasMore?: boolean; + }; +} + +/** + * A successful response from the server for a {@link CompleteRequest | completion/complete} request. + * + * @example Completion result response + * {@includeCode ./examples/CompleteResultResponse/completion-result-response.json} + * + * @category `completion/complete` + */ +export interface CompleteResultResponse extends JSONRPCResultResponse { + result: CompleteResult; +} + +/** + * A reference to a resource or resource template definition. + * + * @category `completion/complete` + */ +export interface ResourceTemplateReference { + type: "ref/resource"; + /** + * The URI or URI template of the resource. + * + * @format uri-template + */ + uri: string; +} + +/** + * Identifies a prompt. + * + * @category `completion/complete` + */ +export interface PromptReference extends BaseMetadata { + type: "ref/prompt"; +} + +/* Roots */ +/** + * Sent from the server to request a list of root URIs from the client. Roots allow + * servers to ask for specific directories or files to operate on. A common example + * for roots is providing a set of repositories or directories a server should operate + * on. + * + * This request is typically used when the server needs to understand the file system + * structure or access specific locations that the client has permission to read from. + * + * @example List roots request + * {@includeCode ./examples/ListRootsRequest/list-roots-request.json} + * + * @deprecated Deprecated as of protocol version 2026-07-28 (SEP-2577). + * Remains in the specification for at least twelve months; see the + * deprecated features registry. + * + * @category `roots/list` + */ +export interface ListRootsRequest { + method: "roots/list"; + params?: { + _meta?: MetaObject; + }; +} + +/** + * The result returned by the client for a {@link ListRootsRequest | roots/list} request. + * This result contains an array of {@link Root} objects, each representing a root directory + * or file that the server can operate on. + * + * @example Single root directory + * {@includeCode ./examples/ListRootsResult/single-root-directory.json} + * + * @example Multiple root directories + * {@includeCode ./examples/ListRootsResult/multiple-root-directories.json} + * + * @deprecated Deprecated as of protocol version 2026-07-28 (SEP-2577). + * Remains in the specification for at least twelve months; see the + * deprecated features registry. + * + * @category `roots/list` + */ +export interface ListRootsResult { + roots: Root[]; +} + +/** + * Represents a root directory or file that the server can operate on. + * + * @example Project directory root + * {@includeCode ./examples/Root/project-directory.json} + * + * @deprecated Deprecated as of protocol version 2026-07-28 (SEP-2577). + * Remains in the specification for at least twelve months; see the + * deprecated features registry. + * + * @category `roots/list` + */ +export interface Root { + /** + * The URI identifying the root. This *must* start with `file://` for now. + * This restriction may be relaxed in future versions of the protocol to allow + * other URI schemes. + * + * @format uri + */ + uri: string; + /** + * An optional name for the root. This can be used to provide a human-readable + * identifier for the root, which may be useful for display purposes or for + * referencing the root in other parts of the application. + */ + name?: string; + + _meta?: MetaObject; +} + +/** + * The parameters for a request to elicit non-sensitive information from the user via a form in the client. + * + * @example Elicit single field + * {@includeCode ./examples/ElicitRequestFormParams/elicit-single-field.json} + * + * @example Elicit multiple fields + * {@includeCode ./examples/ElicitRequestFormParams/elicit-multiple-fields.json} + * + * @category `elicitation/create` + */ +export interface ElicitRequestFormParams { + /** + * The elicitation mode. + */ + mode?: "form"; + + /** + * The message to present to the user describing what information is being requested. + */ + message: string; + + /** + * A restricted subset of JSON Schema. + * Only top-level properties are allowed, without nesting. + */ + requestedSchema: { + $schema?: string; + type: "object"; + properties: { + [key: string]: PrimitiveSchemaDefinition; + }; + required?: string[]; + }; +} + +/** + * The parameters for a request to elicit information from the user via a URL in the client. + * + * @example Elicit sensitive data + * {@includeCode ./examples/ElicitRequestURLParams/elicit-sensitive-data.json} + * + * @category `elicitation/create` + */ +export interface ElicitRequestURLParams { + /** + * The elicitation mode. + */ + mode: "url"; + + /** + * The message to present to the user explaining why the interaction is needed. + */ + message: string; + + /** + * The URL that the user should navigate to. + * + * @format uri + */ + url: string; +} + +/** + * The parameters for a request to elicit additional information from the user via the client. + * + * @category `elicitation/create` + */ +export type ElicitRequestParams = + ElicitRequestFormParams | ElicitRequestURLParams; + +/** + * A request from the server to elicit additional information from the user via the client. + * + * @example Elicitation request + * {@includeCode ./examples/ElicitRequest/elicitation-request.json} + * + * @category `elicitation/create` + */ +export interface ElicitRequest { + method: "elicitation/create"; + params: ElicitRequestParams; +} + +/** + * Restricted schema definitions that only allow primitive types + * without nested objects or arrays. + * + * @category `elicitation/create` + */ +export type PrimitiveSchemaDefinition = + StringSchema | NumberSchema | BooleanSchema | EnumSchema; + +/** + * @example Email input schema + * {@includeCode ./examples/StringSchema/email-input-schema.json} + * + * @category `elicitation/create` + */ +export interface StringSchema { + type: "string"; + title?: string; + description?: string; + minLength?: number; + maxLength?: number; + format?: "email" | "uri" | "date" | "date-time"; + default?: string; +} + +/** + * @example Number input schema + * {@includeCode ./examples/NumberSchema/number-input-schema.json} + * + * @category `elicitation/create` + */ +export interface NumberSchema { + type: "number" | "integer"; + title?: string; + description?: string; + /** + * @TJS-type number + */ + minimum?: number; + /** + * @TJS-type number + */ + maximum?: number; + /** + * @TJS-type number + */ + default?: number; +} + +/** + * @example Boolean input schema + * {@includeCode ./examples/BooleanSchema/boolean-input-schema.json} + * + * @category `elicitation/create` + */ +export interface BooleanSchema { + type: "boolean"; + title?: string; + description?: string; + default?: boolean; +} + +/** + * Schema for single-selection enumeration without display titles for options. + * + * @example Color select schema + * {@includeCode ./examples/UntitledSingleSelectEnumSchema/color-select-schema.json} + * + * @category `elicitation/create` + */ +export interface UntitledSingleSelectEnumSchema { + type: "string"; + /** + * Optional title for the enum field. + */ + title?: string; + /** + * Optional description for the enum field. + */ + description?: string; + /** + * Array of enum values to choose from. + */ + enum: string[]; + /** + * Optional default value. + */ + default?: string; +} + +/** + * Schema for single-selection enumeration with display titles for each option. + * + * @example Titled color select schema + * {@includeCode ./examples/TitledSingleSelectEnumSchema/titled-color-select-schema.json} + * + * @category `elicitation/create` + */ +export interface TitledSingleSelectEnumSchema { + type: "string"; + /** + * Optional title for the enum field. + */ + title?: string; + /** + * Optional description for the enum field. + */ + description?: string; + /** + * Array of enum options with values and display labels. + */ + oneOf: Array<{ + /** + * The enum value. + */ + const: string; + /** + * Display label for this option. + */ + title: string; + }>; + /** + * Optional default value. + */ + default?: string; +} + +/** + * @category `elicitation/create` + */ +// Combined single selection enumeration +export type SingleSelectEnumSchema = + UntitledSingleSelectEnumSchema | TitledSingleSelectEnumSchema; + +/** + * Schema for multiple-selection enumeration without display titles for options. + * + * @example Color multi-select schema + * {@includeCode ./examples/UntitledMultiSelectEnumSchema/color-multi-select-schema.json} + * + * @category `elicitation/create` + */ +export interface UntitledMultiSelectEnumSchema { + type: "array"; + /** + * Optional title for the enum field. + */ + title?: string; + /** + * Optional description for the enum field. + */ + description?: string; + /** + * Minimum number of items to select. + */ + minItems?: number; + /** + * Maximum number of items to select. + */ + maxItems?: number; + /** + * Schema for the array items. + */ + items: { + type: "string"; + /** + * Array of enum values to choose from. + */ + enum: string[]; + }; + /** + * Optional default value. + */ + default?: string[]; +} + +/** + * Schema for multiple-selection enumeration with display titles for each option. + * + * @example Titled color multi-select schema + * {@includeCode ./examples/TitledMultiSelectEnumSchema/titled-color-multi-select-schema.json} + * + * @category `elicitation/create` + */ +export interface TitledMultiSelectEnumSchema { + type: "array"; + /** + * Optional title for the enum field. + */ + title?: string; + /** + * Optional description for the enum field. + */ + description?: string; + /** + * Minimum number of items to select. + */ + minItems?: number; + /** + * Maximum number of items to select. + */ + maxItems?: number; + /** + * Schema for array items with enum options and display labels. + */ + items: { + /** + * Array of enum options with values and display labels. + */ + anyOf: Array<{ + /** + * The constant enum value. + */ + const: string; + /** + * Display title for this option. + */ + title: string; + }>; + }; + /** + * Optional default value. + */ + default?: string[]; +} + +/** + * @category `elicitation/create` + */ +// Combined multiple selection enumeration +export type MultiSelectEnumSchema = + UntitledMultiSelectEnumSchema | TitledMultiSelectEnumSchema; + +/** + * Use {@link TitledSingleSelectEnumSchema} instead. + * This interface will be removed in a future version. + * + * @category `elicitation/create` + */ +export interface LegacyTitledEnumSchema { + type: "string"; + title?: string; + description?: string; + enum: string[]; + /** + * (Legacy) Display names for enum values. + * Non-standard according to JSON schema 2020-12. + */ + enumNames?: string[]; + default?: string; +} + +/** + * @category `elicitation/create` + */ +// Union type for all enum schemas +export type EnumSchema = + SingleSelectEnumSchema | MultiSelectEnumSchema | LegacyTitledEnumSchema; + +/** + * The result returned by the client for an {@link ElicitRequest| elicitation/create} request. + * + * @example Input single field + * {@includeCode ./examples/ElicitResult/input-single-field.json} + * + * @example Input multiple fields + * {@includeCode ./examples/ElicitResult/input-multiple-fields.json} + * + * @example Accept URL mode (no content) + * {@includeCode ./examples/ElicitResult/accept-url-mode-no-content.json} + * + * @category `elicitation/create` + */ +export interface ElicitResult { + /** + * The user action in response to the elicitation. + * - `"accept"`: User submitted the form/confirmed the action + * - `"decline"`: User explicitly declined the action + * - `"cancel"`: User dismissed without making an explicit choice + */ + action: "accept" | "decline" | "cancel"; + + /** + * The submitted form data, only present when action is `"accept"` and mode was `"form"`. + * Contains values matching the requested schema. + * Omitted for out-of-band mode responses. + */ + content?: { [key: string]: string | number | boolean | string[] }; +} + +/* Client messages */ +/** @internal */ +export type ClientRequest = + | DiscoverRequest + | CompleteRequest + | GetPromptRequest + | ListPromptsRequest + | ListResourcesRequest + | ListResourceTemplatesRequest + | ReadResourceRequest + | SubscriptionsListenRequest + | CallToolRequest + | ListToolsRequest; + +/** @internal */ +export type ClientNotification = CancelledNotification; + +/** @internal */ +export type ClientResult = EmptyResult; + +/* Server messages */ + +/** @internal */ +export type ServerNotification = + | CancelledNotification + | ProgressNotification + | LoggingMessageNotification + | ResourceUpdatedNotification + | ResourceListChangedNotification + | ToolListChangedNotification + | PromptListChangedNotification + | SubscriptionsAcknowledgedNotification; + +/** @internal */ +export type ServerResult = + | EmptyResult + | DiscoverResult + | CompleteResult + | GetPromptResult + | ListPromptsResult + | ListResourceTemplatesResult + | ListResourcesResult + | ReadResourceResult + | SubscriptionsListenResult + | CallToolResult + | ListToolsResult + | InputRequiredResult; diff --git a/src/spec-types/README.md b/src/spec-types/README.md index f1318747..5aa12d85 100644 --- a/src/spec-types/README.md +++ b/src/spec-types/README.md @@ -5,10 +5,11 @@ Vendored copies of `schema/{version}/schema.ts` and [modelcontextprotocol](https://github.com/modelcontextprotocol/modelcontextprotocol) spec repository. -The `.ts` files are the canonical TypeScript types for each protocol version. +The `.ts` files are the canonical TypeScript types for each protocol version +(`draft` tracks `schema/draft/`, the revision after the latest release). The conformance suite imports types from here rather than from -`@modelcontextprotocol/sdk` so that it can test draft spec versions before any -SDK has implemented them. +`@modelcontextprotocol/sdk` so that it can test spec versions before any SDK +has implemented them. The `.schema.json` files are the matching JSON Schemas; `src/validation` compiles them (per version) to validate every JSON-RPC message the harness @@ -20,7 +21,46 @@ sends or receives at runtime. npm run sync-schema -- ``` -The `SOURCE` file records the spec commit the current copies came from. +The script vendors every entry of `DATED_SPEC_VERSIONS` (src/types.ts) plus +`draft`, regenerates `schemas.ts` (the version → JSON Schema map the wire +validator uses), and records the spec commit in `SOURCE`. +`DRAFT_PROTOCOL_VERSION` is read from the vendored `draft.ts`, so a draft +marker bump upstream is just a re-run of this script. + +## Spec lifecycle checklist + +**The spec repo bumps the draft's `LATEST_PROTOCOL_VERSION`** (e.g. to a +`DRAFT-YYYY-vN` marker): `npm run sync-schema -- `. Nothing else. + +**A requirement lands in `docs/specification/draft/` and gets a scenario**: +tag it `source: { introducedIn: DRAFT_SPEC_VERSION }` (or +`removedIn: DRAFT_SPEC_VERSION` for something the draft removes). It runs under +`--spec-version draft` / `--suite draft` and stays out of the default suites +and tier scoring. + +**A new revision `YYYY-MM-DD` is released**: + +1. Append `'YYYY-MM-DD'` to `DATED_SPEC_VERSIONS` in `src/types.ts` + (`LATEST_SPEC_VERSION` follows automatically). If the revision keeps the + stateful initialize lifecycle, also add it to `STATEFUL_VERSIONS` in + `src/connection/select.ts`; otherwise it is stateless by default. +2. `npm run sync-schema -- ` (vendors `schema/YYYY-MM-DD`, + regenerates `schemas.ts`). +3. Retag the scenarios that shipped: `DRAFT_SPEC_VERSION` → `'YYYY-MM-DD'` in + `introducedIn`/`removedIn` for every requirement the release includes + (`rg DRAFT_SPEC_VERSION src/scenarios`). Anything still unreleased keeps + the draft tag. +4. Freeze `requirements/YYYY-MM-DD.yaml` (start from + `conformance list --spec-version YYYY-MM-DD`; see the header comments in + the existing files for what goes in `not_scored`). A test insists the + latest dated version has one. +5. Add `specOverrides['YYYY-MM-DD']` entries in `src/sdk-runner/known-sdks.ts` + for SDKs that need a different invocation at the new wire, and skim + README examples that name the latest revision. + +Help text, the draft-suite/`--spec-version draft` notes, the stateless +version lists, the tier-check matrix columns and the wire validator all +derive from `DATED_SPEC_VERSIONS` and need no edits. ## Import rule @@ -31,4 +71,4 @@ import type { ListToolsResult } from '../../spec-types/2025-06-18'; ``` `Connection` implementations import the version whose lifecycle they implement -(stateful → `2025-11-25`, stateless → `draft`). +(stateful → `2025-11-25`, stateless → `2026-07-28`). diff --git a/src/spec-types/SOURCE b/src/spec-types/SOURCE index a4891c9d..9fa90fe5 100644 --- a/src/spec-types/SOURCE +++ b/src/spec-types/SOURCE @@ -1 +1 @@ -modelcontextprotocol@71e306956a4959c9655e5036be215d41986596e6 +modelcontextprotocol@5f5440bb26a62e2cf3440b92da5a667efa03b267 diff --git a/src/spec-types/draft.schema.json b/src/spec-types/draft.schema.json index cc44564e..213c58f6 100644 --- a/src/spec-types/draft.schema.json +++ b/src/spec-types/draft.schema.json @@ -3400,7 +3400,7 @@ "description": "The response to a {@link SubscriptionsListenRequestsubscriptions/listen}\nrequest, signalling that the subscription has ended gracefully (for example,\nduring server shutdown). Because the listen stream is long-lived, this result\nis sent only when the server tears the subscription down; an abrupt transport\nclose carries no response. The result body is otherwise empty.", "properties": { "_meta": { - "$ref": "#/$defs/SubscriptionsListenResultMeta" + "$ref": "#/$defs/SubscriptionsListenResultMetaObject" }, "resultType": { "description": "Indicates the type of the result, which allows the client to determine\nhow to parse the result object.\n\nServers implementing this protocol version MUST include this field.\nFor backward compatibility, when a client receives a result from a\nserver implementing an earlier protocol version (which does not include\n`resultType`), the client MUST treat the absent field as `\"complete\"`.", @@ -3413,7 +3413,7 @@ ], "type": "object" }, - "SubscriptionsListenResultMeta": { + "SubscriptionsListenResultMetaObject": { "description": "Extends {@link ResultMetaObject} with the subscription-stream identifier carried by a\n{@link SubscriptionsListenResult}. All key naming rules from `MetaObject` apply.", "properties": { "io.modelcontextprotocol/serverInfo": { @@ -3430,6 +3430,27 @@ ], "type": "object" }, + "SubscriptionsListenResultResponse": { + "description": "A successful response from the server for a {@link SubscriptionsListenRequestsubscriptions/listen}\nrequest, sent when the server tears the subscription down gracefully.", + "properties": { + "id": { + "$ref": "#/$defs/RequestId" + }, + "jsonrpc": { + "const": "2.0", + "type": "string" + }, + "result": { + "$ref": "#/$defs/SubscriptionsListenResult" + } + }, + "required": [ + "id", + "jsonrpc", + "result" + ], + "type": "object" + }, "TextContent": { "description": "Text provided to or from an LLM.", "properties": { diff --git a/src/spec-types/draft.ts b/src/spec-types/draft.ts index 110485f6..b2119e14 100644 --- a/src/spec-types/draft.ts +++ b/src/spec-types/draft.ts @@ -1323,7 +1323,7 @@ export interface SubscriptionsListenRequest extends JSONRPCRequest { * @see {@link MetaObject} for key naming rules and reserved prefixes. * @category `subscriptions/listen` */ -export interface SubscriptionsListenResultMeta extends ResultMetaObject { +export interface SubscriptionsListenResultMetaObject extends ResultMetaObject { /** * Identifies the subscription stream this response closes, so the client can * correlate it with the originating subscription — mirroring the same key on @@ -1347,7 +1347,20 @@ export interface SubscriptionsListenResultMeta extends ResultMetaObject { * @category `subscriptions/listen` */ export interface SubscriptionsListenResult extends Result { - _meta: SubscriptionsListenResultMeta; + _meta: SubscriptionsListenResultMetaObject; +} + +/** + * A successful response from the server for a {@link SubscriptionsListenRequest | subscriptions/listen} + * request, sent when the server tears the subscription down gracefully. + * + * @example Subscription closed gracefully response + * {@includeCode ./examples/SubscriptionsListenResultResponse/listen-closed-response.json} + * + * @category `subscriptions/listen` + */ +export interface SubscriptionsListenResultResponse extends JSONRPCResultResponse { + result: SubscriptionsListenResult; } /** diff --git a/src/spec-types/schemas.ts b/src/spec-types/schemas.ts new file mode 100644 index 00000000..8950f969 --- /dev/null +++ b/src/spec-types/schemas.ts @@ -0,0 +1,17 @@ +// Generated by scripts/sync-schema.ts from DATED_SPEC_VERSIONS + draft. +// Do not edit: add the version to src/types.ts and re-run the script. +import type { SpecVersion } from '../types'; +import schema2025_03_26 from './2025-03-26.schema.json'; +import schema2025_06_18 from './2025-06-18.schema.json'; +import schema2025_11_25 from './2025-11-25.schema.json'; +import schema2026_07_28 from './2026-07-28.schema.json'; +import schemaDraft from './draft.schema.json'; + +/** Vendored JSON Schema for every spec version the suite can target. */ +export const SPEC_SCHEMAS: Record> = { + '2025-03-26': schema2025_03_26, + '2025-06-18': schema2025_06_18, + '2025-11-25': schema2025_11_25, + '2026-07-28': schema2026_07_28, + 'draft': schemaDraft, +}; diff --git a/src/tier-check/checks/test-conformance-results.ts b/src/tier-check/checks/test-conformance-results.ts index 2de317b0..9cefe1ff 100644 --- a/src/tier-check/checks/test-conformance-results.ts +++ b/src/tier-check/checks/test-conformance-results.ts @@ -19,15 +19,12 @@ import { } from '../../requirements'; import { ConformanceCheck, - DRAFT_PROTOCOL_VERSION, + DRAFT_SPEC_VERSION, ScenarioSpecTag, SpecVersion } from '../../types'; -const NON_SCORING_TAGS: ScenarioSpecTag[] = [ - DRAFT_PROTOCOL_VERSION, - 'extension' -]; +const NON_SCORING_TAGS: ScenarioSpecTag[] = [DRAFT_SPEC_VERSION, 'extension']; /** Whether a scenario counts toward tier scoring (has at least one date-versioned spec). */ function isTierScoring(specVersions?: ScenarioSpecTag[]): boolean { diff --git a/src/tier-check/index.ts b/src/tier-check/index.ts index db1f4b1a..d9273dcc 100644 --- a/src/tier-check/index.ts +++ b/src/tier-check/index.ts @@ -276,6 +276,23 @@ export function createTierCheckCommand(): Command { `No --requirements given; assessing every shipped revision: ${revisions.join(', ')}` ); requirements = revisions.map(loadRequirements); + } else if (!specVersion && !options.skipConformance) { + // No target named at all. Score against the latest shipped revision's + // frozen requirement set rather than "the suite as it stands today": + // that keeps legacy-only scenarios (removed before this revision) and + // extensions out of the denominator, and makes a bare run mean the + // same thing from one release of this tool to the next. One URL can + // only be assumed to speak one wire, so unlike --sdk this claims the + // latest revision alone; pass --requirements to claim more. + const revisions = listRequirementRevisions(); + const latest = revisions[revisions.length - 1]; + if (latest) { + console.error( + `No --requirements or --spec-version given; scoring against the latest shipped revision (${latest}). ` + + `Pass --requirements ${revisions.join(',')} to claim every revision, or --spec-version for an unfrozen run.` + ); + requirements = [loadRequirements(latest)]; + } } if (!token) { diff --git a/src/tier-check/output.ts b/src/tier-check/output.ts index 95686c28..e5a6c3df 100644 --- a/src/tier-check/output.ts +++ b/src/tier-check/output.ts @@ -1,5 +1,5 @@ import { TierScorecard, CheckStatus, ConformanceResult } from './types'; -import { DATED_SPEC_VERSIONS, DRAFT_PROTOCOL_VERSION } from '../types'; +import { DATED_SPEC_VERSIONS, DRAFT_SPEC_VERSION } from '../types'; const COLORS = { RESET: '\x1b[0m', @@ -26,7 +26,7 @@ function statusIcon(status: CheckStatus): string { const TIER_SPEC_VERSIONS = DATED_SPEC_VERSIONS; -const INFO_SPEC_VERSIONS = [DRAFT_PROTOCOL_VERSION, 'extension'] as const; +const INFO_SPEC_VERSIONS = [DRAFT_SPEC_VERSION, 'extension'] as const; type Cell = { passed: number; total: number }; @@ -74,6 +74,10 @@ function buildConformanceMatrix( bucket.total++; if (d.passed) bucket.passed++; for (const v of versions) { + // The informational columns list what does NOT count. A scored + // scenario that merely also applies at the draft is not informational, + // so it stays out of the draft column rather than appearing twice. + if (isTierScoring && INFO_SET.has(v)) continue; const cell = row.cells.get(v) ?? { passed: 0, total: 0 }; cell.total++; if (d.passed) cell.passed++; diff --git a/src/types.test.ts b/src/types.test.ts index 6612196e..17dfc2f6 100644 --- a/src/types.test.ts +++ b/src/types.test.ts @@ -1,19 +1,53 @@ import { describe, it, expect } from 'vitest'; import { LATEST_PROTOCOL_VERSION } from './spec-types/draft'; -import { DATED_SPEC_VERSIONS, DRAFT_PROTOCOL_VERSION } from './types'; +import { + DATED_SPEC_VERSIONS, + DRAFT_PROTOCOL_VERSION, + DRAFT_SPEC_VERSION, + LATEST_SPEC_VERSION, + SPEC_VERSION_TIMELINE, + protocolVersionFor, + specVersionAtLeast +} from './types'; describe('DRAFT_PROTOCOL_VERSION', () => { it('mirrors LATEST_PROTOCOL_VERSION from the vendored draft schema', () => { - // DRAFT_PROTOCOL_VERSION is the wire protocolVersion the harness asserts - // for draft-spec scenarios. It must match what the spec's draft schema - // declares, or SDKs implementing the draft cannot pass the draft suite. + // DRAFT_PROTOCOL_VERSION is the wire protocolVersion the harness sends for + // `--spec-version draft`. It must match what the spec's draft schema + // declares, or SDKs implementing the draft cannot pass a draft run. expect(DRAFT_PROTOCOL_VERSION).toBe(LATEST_PROTOCOL_VERSION); }); +}); - it('is distinct from every dated spec version', () => { +describe('DRAFT_SPEC_VERSION', () => { + it('is distinct from every dated spec version and last on the timeline', () => { // Scenario applicability (introducedIn/removedIn) and --spec-version // resolution rely on the draft identifier not colliding with a released - // version. - expect(DATED_SPEC_VERSIONS).not.toContain(DRAFT_PROTOCOL_VERSION); + // version, even while its wire string does. + expect(DATED_SPEC_VERSIONS).not.toContain(DRAFT_SPEC_VERSION); + expect(SPEC_VERSION_TIMELINE[SPEC_VERSION_TIMELINE.length - 1]).toBe( + DRAFT_SPEC_VERSION + ); + expect(specVersionAtLeast(DRAFT_SPEC_VERSION, LATEST_SPEC_VERSION)).toBe( + true + ); + expect(specVersionAtLeast(LATEST_SPEC_VERSION, DRAFT_SPEC_VERSION)).toBe( + false + ); + }); + + it('maps to the draft wire version; dated versions map to themselves', () => { + expect(protocolVersionFor(DRAFT_SPEC_VERSION)).toBe(DRAFT_PROTOCOL_VERSION); + for (const v of DATED_SPEC_VERSIONS) { + expect(protocolVersionFor(v)).toBe(v); + } + }); +}); + +describe('LATEST_SPEC_VERSION', () => { + it('is the last dated spec version', () => { + expect(DATED_SPEC_VERSIONS[DATED_SPEC_VERSIONS.length - 1]).toBe( + LATEST_SPEC_VERSION + ); }); }); diff --git a/src/types.ts b/src/types.ts index 5960945b..a5d26fb8 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,6 +1,7 @@ import type { RunContext } from './connection'; import type { ScenarioContext } from './mock-server'; import type { AuthorizationServerOptions } from './schemas'; +import { LATEST_PROTOCOL_VERSION as DRAFT_SCHEMA_PROTOCOL_VERSION } from './spec-types/draft'; export type CheckStatus = | 'SUCCESS' @@ -32,41 +33,81 @@ export interface ConformanceCheck { logs?: string[]; } +/** + * Released spec revisions the suite can target, oldest first. Publishing a + * new revision means appending its date here and running + * `npm run sync-schema -- `; see src/spec-types/README.md for + * the full checklist. The last entry is {@link LATEST_SPEC_VERSION}. + */ export const DATED_SPEC_VERSIONS = [ '2025-03-26', '2025-06-18', - '2025-11-25' + '2025-11-25', + '2026-07-28' ] as const; export type DatedSpecVersion = (typeof DATED_SPEC_VERSIONS)[number]; -export const LATEST_SPEC_VERSION: DatedSpecVersion = '2025-11-25'; +type Last = T extends readonly [ + ...unknown[], + infer L +] + ? L + : never; + +/** The most recent released revision: the last entry of {@link DATED_SPEC_VERSIONS}. */ +export const LATEST_SPEC_VERSION = DATED_SPEC_VERSIONS[ + DATED_SPEC_VERSIONS.length - 1 +] as Last; /** - * Wire `protocolVersion` for the in-progress spec. Mirrors - * `LATEST_PROTOCOL_VERSION` in the spec repo's `schema/draft/schema.ts`; - * bump when that constant changes. + * Names the in-progress spec revision: whatever `docs/specification/draft/` + * in the spec repo will become after {@link LATEST_SPEC_VERSION}. It sits + * last on the timeline, so a scenario `introducedIn: DRAFT_SPEC_VERSION` runs + * only under `--spec-version draft`, and one `removedIn: DRAFT_SPEC_VERSION` + * runs at every dated revision but not the draft. */ -export const DRAFT_PROTOCOL_VERSION = '2026-07-28'; +export const DRAFT_SPEC_VERSION = 'draft'; -// Wire protocolVersion strings the mock server will negotiate on initialize. -export const NEGOTIABLE_PROTOCOL_VERSIONS: readonly string[] = [ - '2025-06-18', - LATEST_SPEC_VERSION, - DRAFT_PROTOCOL_VERSION -]; +/** + * Wire `protocolVersion` the draft currently declares: `LATEST_PROTOCOL_VERSION` + * from the vendored `schema/draft/schema.ts`, so it tracks the spec repo with + * `npm run sync-schema` and needs no edit here. Right after a release the spec + * repo leaves this equal to the released revision until the next draft gets + * its own marker, so it can coincide with {@link LATEST_SPEC_VERSION}; + * {@link DRAFT_SPEC_VERSION} is what keeps the draft distinct on the timeline + * regardless. + */ +export const DRAFT_PROTOCOL_VERSION: string = DRAFT_SCHEMA_PROTOCOL_VERSION; /** - * A spec revision the conformance suite can target via `--spec-version`. - * Always a wire `protocolVersion` string. The CLI also accepts `'draft'` as - * an alias for {@link DRAFT_PROTOCOL_VERSION}. + * A spec revision the conformance suite can target via `--spec-version`: + * a dated release, or `'draft'` for the revision after the latest release. + * For dated revisions the value is also the wire `protocolVersion`; use + * {@link protocolVersionFor} wherever a value goes on the wire so the draft + * maps to {@link DRAFT_PROTOCOL_VERSION}. */ -export type SpecVersion = DatedSpecVersion | typeof DRAFT_PROTOCOL_VERSION; +export type SpecVersion = DatedSpecVersion | typeof DRAFT_SPEC_VERSION; /** Spec versions in timeline order, dated revisions followed by the draft. */ export const SPEC_VERSION_TIMELINE: readonly SpecVersion[] = [ ...DATED_SPEC_VERSIONS, - DRAFT_PROTOCOL_VERSION + DRAFT_SPEC_VERSION +]; + +/** Wire `protocolVersion` string to send/expect when a run targets `v`. */ +export function protocolVersionFor(v: SpecVersion): string { + return v === DRAFT_SPEC_VERSION ? DRAFT_PROTOCOL_VERSION : v; +} + +// Wire protocolVersion strings the mock server will negotiate on initialize. +export const NEGOTIABLE_PROTOCOL_VERSIONS: readonly string[] = [ + ...new Set([ + '2025-06-18', + '2025-11-25', + LATEST_SPEC_VERSION, + DRAFT_PROTOCOL_VERSION + ]) ]; /** True when `v` names a known spec version (dated or draft). */ @@ -76,8 +117,9 @@ export function isSpecVersion(v: unknown): v is SpecVersion { /** * True when `v` is at or after `threshold` on the spec timeline. Lets a check - * gate itself to the version that introduced its requirement (e.g. a draft-only - * requirement passes `DRAFT_PROTOCOL_VERSION` as the threshold). + * gate itself to the version that introduced its requirement (e.g. a + * requirement added in 2026-07-28 passes `'2026-07-28'` as the threshold and + * is then also enforced under the draft). */ export function specVersionAtLeast( v: SpecVersion, @@ -113,8 +155,8 @@ export type ExtensionId = (typeof EXTENSION_IDS)[number]; */ export type ScenarioSource = | { - introducedIn: DatedSpecVersion | typeof DRAFT_PROTOCOL_VERSION; - removedIn?: DatedSpecVersion | typeof DRAFT_PROTOCOL_VERSION; + introducedIn: SpecVersion; + removedIn?: SpecVersion; } | { extensionId: ExtensionId }; diff --git a/src/validation/wire-schema.test.ts b/src/validation/wire-schema.test.ts index 98fc4d7b..a3a8ab32 100644 --- a/src/validation/wire-schema.test.ts +++ b/src/validation/wire-schema.test.ts @@ -1,7 +1,4 @@ import { createServer, type Server } from 'http'; -import rawSchema2025_06_18 from '../spec-types/2025-06-18.schema.json'; -import rawSchema2025_11_25 from '../spec-types/2025-11-25.schema.json'; -import { DRAFT_PROTOCOL_VERSION } from '../types'; import { sendStatelessRequest } from '../connection/stateless'; import { resetWireValidation, @@ -13,8 +10,11 @@ import { withWireRecorder } from './wire-schema'; +// The stateless-lifecycle schema under test is the 2026-07-28 release. +const STATELESS = '2026-07-28' as const; + const META = { - 'io.modelcontextprotocol/protocolVersion': DRAFT_PROTOCOL_VERSION, + 'io.modelcontextprotocol/protocolVersion': STATELESS, 'io.modelcontextprotocol/clientInfo': { name: 'c', version: '1' }, 'io.modelcontextprotocol/clientCapabilities': { sampling: {} } }; @@ -52,11 +52,8 @@ function listen( } describe('wireSchemaErrors', () => { - it('rejects the PR #376 hallucinated requiredCapabilities array under the draft schema', () => { - const errors = wireSchemaErrors( - DRAFT_PROTOCOL_VERSION, - PR_376_INVALID_ERROR - ); + it('rejects the PR #376 hallucinated requiredCapabilities array under the 2026-07-28 schema', () => { + const errors = wireSchemaErrors(STATELESS, PR_376_INVALID_ERROR); expect(errors).toEqual([ expect.stringContaining( 'MissingRequiredClientCapabilityError/error/data/requiredCapabilities: must be object' @@ -66,7 +63,7 @@ describe('wireSchemaErrors', () => { it('accepts the spec-valid ClientCapabilities object shape', () => { expect( - wireSchemaErrors(DRAFT_PROTOCOL_VERSION, { + wireSchemaErrors(STATELESS, { jsonrpc: '2.0', id: 1, error: { @@ -86,7 +83,7 @@ describe('wireSchemaErrors', () => { it('validates typed requests by method const', () => { expect( - wireSchemaErrors(DRAFT_PROTOCOL_VERSION, { + wireSchemaErrors(STATELESS, { jsonrpc: '2.0', id: 2, method: 'tools/call', @@ -100,9 +97,9 @@ describe('wireSchemaErrors', () => { }); it('validates results against the typed result definition for the request method', () => { - // Draft results must carry resultType / ttlMs / cacheScope. + // 2026-07-28 results must carry resultType / ttlMs / cacheScope. const errors = wireSchemaErrors( - DRAFT_PROTOCOL_VERSION, + STATELESS, { jsonrpc: '2.0', id: 3, result: { tools: [] } }, 'tools/list' ); @@ -142,7 +139,7 @@ describe('wire-schema choke points and checks', () => { try { resetWireValidation(); await sendStatelessRequest(url, 'tools/list'); - const checks = wireSchemaChecks(DRAFT_PROTOCOL_VERSION); + const checks = wireSchemaChecks(STATELESS); const wire = checks.find((c) => c.id === 'wire-schema-valid'); expect(wire?.status).toBe('FAILURE'); expect(wire?.errorMessage).toContain('requiredCapabilities'); @@ -169,7 +166,7 @@ describe('wire-schema choke points and checks', () => { resetWireValidation(); // tools/call without params.name violates CallToolRequest. await sendStatelessRequest(url, 'tools/call', { arguments: {} }); - const checks = wireSchemaChecks(DRAFT_PROTOCOL_VERSION); + const checks = wireSchemaChecks(STATELESS); const harness = checks.find((c) => c.id === 'wire-schema-harness-error'); expect(harness?.status).toBe('FAILURE'); expect(harness?.errorMessage).toContain('HARNESS ERROR'); @@ -256,7 +253,7 @@ describe('wire-schema choke points and checks', () => { try { resetWireValidation(); await sendStatelessRequest(url, 'tools/list'); - const checks = wireSchemaChecks(DRAFT_PROTOCOL_VERSION); + const checks = wireSchemaChecks(STATELESS); expect(checks).toHaveLength(1); expect(checks[0].id).toBe('wire-schema-valid'); expect(checks[0].status).toBe('SUCCESS'); @@ -268,7 +265,7 @@ describe('wire-schema choke points and checks', () => { it('emits no checks when no wire traffic was observed', () => { resetWireValidation(); - expect(wireSchemaChecks(DRAFT_PROTOCOL_VERSION)).toEqual([]); + expect(wireSchemaChecks(STATELESS)).toEqual([]); }); }); @@ -322,10 +319,10 @@ describe('specDispatchMaps', () => { 'notifications/tasks/status': 'TaskStatusNotification' }; - // The draft (SEP-2575) drops the initialize/session lifecycle, ping, + // 2026-07-28 (SEP-2575) drops the initialize/session lifecycle, ping, // logging/setLevel, roots-changed, tasks, and resource subscriptions, and // adds server/discover plus subscriptions/listen. - const METHOD_DEFS_DRAFT: Record = (() => { + const METHOD_DEFS_2026: Record = (() => { const defs: Record = { ...METHOD_DEFS_2025_11_25, 'server/discover': 'DiscoverRequest', @@ -362,8 +359,8 @@ describe('specDispatchMaps', () => { { '-32042': 'URLElicitationRequiredError' } ], [ - DRAFT_PROTOCOL_VERSION, - METHOD_DEFS_DRAFT, + STATELESS, + METHOD_DEFS_2026, { '-32020': 'HeaderMismatchError', '-32021': 'MissingRequiredClientCapabilityError', @@ -399,13 +396,11 @@ describe('specDispatchMaps', () => { specDispatchMaps('2025-11-25').resultDefs.get('elicitation/create') ).toBe('ElicitResult'); expect( - specDispatchMaps(DRAFT_PROTOCOL_VERSION).resultDefs.get( - 'elicitation/create' - ) + specDispatchMaps(STATELESS).resultDefs.get('elicitation/create') ).toBe('ElicitResult'); - expect( - specDispatchMaps(DRAFT_PROTOCOL_VERSION).resultDefs.get('server/discover') - ).toBe('DiscoverResult'); + expect(specDispatchMaps(STATELESS).resultDefs.get('server/discover')).toBe( + 'DiscoverResult' + ); }); }); @@ -442,7 +437,7 @@ describe('withWireRecorder', () => { const scopeResult = withWireRecorder(async () => { const server = createServer((_req, res) => { - validateWireMessage(DRAFT_PROTOCOL_VERSION, PR_376_INVALID_ERROR, { + validateWireMessage(STATELESS, PR_376_INVALID_ERROR, { origin: 'implementation', context: 'inbound message to in-scope server' }); @@ -472,7 +467,7 @@ describe('withWireRecorder', () => { }); }); -describe('schema errata', () => { +describe('NumberSchema bounds (generator bug fixed upstream, modelcontextprotocol#2710/#3139)', () => { const elicitWithScore = (score: Record) => ({ jsonrpc: '2.0', id: 1, @@ -483,7 +478,19 @@ describe('schema errata', () => { } }); - it('accepts fractional NumberSchema bounds/defaults at 2025-11-25 (spec generator bug, modelcontextprotocol#2710)', () => { + it.each(['2025-06-18', '2025-11-25'] as const)( + 'accepts fractional NumberSchema bounds at %s from the vendored schema itself', + (version) => { + expect( + wireSchemaErrors( + version, + elicitWithScore({ type: 'number', minimum: 0.5, maximum: 99.5 }) + ) + ).toEqual([]); + } + ); + + it('accepts a fractional NumberSchema default at 2025-11-25', () => { expect( wireSchemaErrors( '2025-11-25', @@ -495,44 +502,5 @@ describe('schema errata', () => { }) ) ).toEqual([]); - // The erratum makes 2025-11-25 agree with the fixed draft schema. - expect( - wireSchemaErrors( - DRAFT_PROTOCOL_VERSION, - elicitWithScore({ - type: 'number', - minimum: 0.5, - maximum: 99.5, - default: 95.5 - }) - ) - ).toEqual([]); - }); - - it('accepts fractional NumberSchema bounds at 2025-06-18 (same generator bug, no default there)', () => { - expect( - wireSchemaErrors( - '2025-06-18', - elicitWithScore({ type: 'number', minimum: 0.5, maximum: 99.5 }) - ) - ).toEqual([]); - }); - - it('tripwire: delete applySchemaErrata once the vendored schemas are fixed (modelcontextprotocol#3139)', () => { - const numberSchema = (raw: Record, defsKey: string) => - ( - raw[defsKey] as Record< - string, - { properties: Record } - > - ).NumberSchema.properties; - expect( - numberSchema(rawSchema2025_11_25, '$defs').default.type, - 'the re-vendored 2025-11-25 schema no longer needs the erratum — delete its applySchemaErrata branch' - ).toBe('integer'); - expect( - numberSchema(rawSchema2025_06_18, 'definitions').minimum.type, - 'the re-vendored 2025-06-18 schema no longer needs the erratum — delete its applySchemaErrata branch' - ).toBe('integer'); }); }); diff --git a/src/validation/wire-schema.ts b/src/validation/wire-schema.ts index 8b45849d..d7b30e71 100644 --- a/src/validation/wire-schema.ts +++ b/src/validation/wire-schema.ts @@ -6,15 +6,8 @@ import { AsyncLocalStorage } from 'node:async_hooks'; import { Ajv, type ValidateFunction, type ErrorObject } from 'ajv'; import { Ajv2020 } from 'ajv/dist/2020.js'; import { default as addFormats } from 'ajv-formats'; -import { - DRAFT_PROTOCOL_VERSION, - type ConformanceCheck, - type SpecVersion -} from '../types'; -import schema2025_03_26 from '../spec-types/2025-03-26.schema.json'; -import schema2025_06_18 from '../spec-types/2025-06-18.schema.json'; -import schema2025_11_25 from '../spec-types/2025-11-25.schema.json'; -import schemaDraft from '../spec-types/draft.schema.json'; +import { type ConformanceCheck, type SpecVersion } from '../types'; +import { SPEC_SCHEMAS } from '../spec-types/schemas'; export type WireOrigin = 'harness' | 'implementation'; @@ -36,16 +29,9 @@ export interface WireSchemaViolation { message: unknown; } -const SCHEMAS: Record> = { - '2025-03-26': schema2025_03_26, - '2025-06-18': schema2025_06_18, - '2025-11-25': schema2025_11_25, - [DRAFT_PROTOCOL_VERSION]: schemaDraft -}; - -/** Spec-repo directory name for a version (the draft is pinned unversioned). */ +/** Spec-repo `schema/` name for a version; `SpecVersion` values already are one. */ export function schemaDirFor(specVersion: SpecVersion): string { - return specVersion === DRAFT_PROTOCOL_VERSION ? 'draft' : specVersion; + return specVersion; } /** Union definitions that alias a single concrete type (and so carry a @@ -75,36 +61,11 @@ interface CompiledSpec { const compiledSpecs = new Map(); -/** Patch known generator bugs in released spec schema.json files at load time - * so validation matches the schema.ts source of truth; delete each branch (a - * test trips) once the dated schema is fixed upstream and re-vendored. */ -function applySchemaErrata( - specVersion: SpecVersion, - schema: Record -): Record { - if (specVersion !== '2025-11-25' && specVersion !== '2025-06-18') { - return schema; - } - // NumberSchema minimum/maximum (plus default at 2025-11-25) are generated - // as `integer`, contradicting schema.ts (`number`). Fixed for draft in - // modelcontextprotocol#2710; dated fixes proposed in modelcontextprotocol#3139. - const patched = structuredClone(schema); - const defs = (patched.$defs ?? patched.definitions) as Record< - string, - { properties?: Record } - >; - for (const prop of ['minimum', 'maximum', 'default']) { - const p = defs.NumberSchema?.properties?.[prop]; - if (p?.type === 'integer') p.type = 'number'; - } - return patched; -} - function compileSpec(specVersion: SpecVersion): CompiledSpec { let compiled = compiledSpecs.get(specVersion); if (compiled) return compiled; - const schema = applySchemaErrata(specVersion, SCHEMAS[specVersion]); + const schema = SPEC_SCHEMAS[specVersion]; const is2020 = typeof schema.$schema === 'string' && schema.$schema.includes('2020-12'); // The spec schemas are not authored for ajv strict mode; validate them