diff --git a/.changeset/codec-era-gates.md b/.changeset/codec-era-gates.md new file mode 100644 index 0000000000..30855b7f87 --- /dev/null +++ b/.changeset/codec-era-gates.md @@ -0,0 +1,7 @@ +--- +'@modelcontextprotocol/core': minor +'@modelcontextprotocol/client': minor +'@modelcontextprotocol/server': minor +--- + +Add `SdkErrorCode.MethodNotSupportedByProtocolVersion`: a typed local error raised before anything reaches the transport when a spec method is sent toward a peer whose negotiated protocol version's wire era does not define it (for example `tasks/get` toward a 2026-07-28 peer). The protocol layer now resolves a per-era wire codec from the connection's negotiated protocol version (instance state on `Client`/`Server`, with the legacy era as the pre-negotiation default) and resolves per-method schemas at dispatch time instead of registration time; an edge classification on an inbound message is validated against that instance era, and a mismatch is rejected as an entry/routing error. Behavior on existing (2025-era) connections is unchanged. diff --git a/.changeset/codec-split-wire-break.md b/.changeset/codec-split-wire-break.md new file mode 100644 index 0000000000..2a20452ab6 --- /dev/null +++ b/.changeset/codec-split-wire-break.md @@ -0,0 +1,15 @@ +--- +'@modelcontextprotocol/core': major +'@modelcontextprotocol/client': major +'@modelcontextprotocol/server': major +--- + +Split the wire layer into per-era codecs and make protocol-revision deletions physical. Deliberate wire/schema behavior changes (see docs/migration.md "Per-era wire codecs"): + +- `resultType` is no longer modeled by any neutral wire schema: `EmptyResultSchema` (strict) now rejects `{resultType}` bodies; on 2025-era connections a foreign `resultType` is stripped before validation instead of rejected; the member exists only inside the 2026-era codec, which requires it. +- `CallToolResult.content` / `ToolResultContent.content` are required at the wire boundary (`content.default([])` removed): handler results without `content` are rejected with `-32602` instead of silently defaulted, and content-less wire results fail the client parse loudly. +- Custom (3-arg) handlers now receive `_meta` minus the reserved envelope keys instead of having it deleted before params validation. +- `specTypeSchemas` re-scoped to the neutral model: result validators no longer accept `resultType`; task message-type validators and `RequestMetaEnvelope` left the public set (`SpecTypeName` narrowed). +- Role aggregate types/schemas (`ClientRequest`, `ServerResult`, …) no longer carry task vocabulary; the deprecated `Task*` types remain importable unchanged. +- Era-mismatched spec methods fail physically: inbound era-deleted methods get `-32601` even with a handler registered; outbound sends throw `SdkErrorCode.MethodNotSupportedByProtocolVersion` locally. +- Value guards (`isCallToolResult`, …) are documented as neutral-shape consumer checks, not wire validators. diff --git a/.changeset/codemod-flag-removed-task-options.md b/.changeset/codemod-flag-removed-task-options.md new file mode 100644 index 0000000000..7eec3cf127 --- /dev/null +++ b/.changeset/codemod-flag-removed-task-options.md @@ -0,0 +1,5 @@ +--- +'@modelcontextprotocol/codemod': patch +--- + +The v1→v2 codemod no longer rewrites `taskStore`/`taskMessageQueue` McpServer constructor options into `capabilities.tasks` — that target does not exist in v2 (the experimental tasks runtime was removed, SEP-2663). The codemod now leaves the code untouched and emits an action-required diagnostic telling migrators to remove the option, matching the removal guidance already given for `experimental/tasks` imports and the migration guide. diff --git a/.changeset/extract-task-manager.md b/.changeset/extract-task-manager.md deleted file mode 100644 index 6a72182837..0000000000 --- a/.changeset/extract-task-manager.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -"@modelcontextprotocol/core": minor -"@modelcontextprotocol/client": minor -"@modelcontextprotocol/server": minor ---- - -refactor: extract task orchestration from Protocol into TaskManager - -**Breaking changes:** -- `taskStore`, `taskMessageQueue`, `defaultTaskPollInterval`, and `maxTaskQueueSize` moved from `ProtocolOptions` to `capabilities.tasks` on `ClientOptions`/`ServerOptions` diff --git a/.changeset/fix-task-session-isolation.md b/.changeset/fix-task-session-isolation.md deleted file mode 100644 index 7220673374..0000000000 --- a/.changeset/fix-task-session-isolation.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@modelcontextprotocol/core': patch ---- - -Fix InMemoryTaskStore to enforce session isolation. Previously, sessionId was accepted but ignored on all TaskStore methods, allowing any session to enumerate, read, and mutate tasks created by other sessions. The store now persists sessionId at creation time and enforces ownership on all reads and writes. diff --git a/.changeset/hide-wire-only-members.md b/.changeset/hide-wire-only-members.md new file mode 100644 index 0000000000..7d241921f5 --- /dev/null +++ b/.changeset/hide-wire-only-members.md @@ -0,0 +1,7 @@ +--- +'@modelcontextprotocol/core': major +'@modelcontextprotocol/client': major +'@modelcontextprotocol/server': major +--- + +Hide wire-only protocol members from the public surface, at the type level and at runtime. `resultType` (the 2026-07-28 result discrimination field) is no longer declared on any public result type — the wire schemas keep parsing it, and the client funnel now consumes it raw-first: `'complete'` results are stripped to the public shape and any other kind (e.g. `input_required`) rejects with the new `SdkErrorCode.UnsupportedResultType` instead of masking into an empty success. The reserved `_meta` envelope keys are lifted out of inbound requests and notifications before handlers run, and the multi-round-trip retry fields (`inputResponses`, `requestState`) out of inbound requests only (the spec reserves those names on client-initiated requests; notification params keep them), so handler params keep the 2025-era shape; for requests the lifted material surfaces at `ctx.mcpReq.envelope`, `ctx.mcpReq.inputResponses`, and `ctx.mcpReq.requestState` (notifications have no ctx — their lifted envelope keys are not surfaced). High-level client/server methods now return the named public result types (`Promise` etc.). Task wire vocabulary stays importable but is `@deprecated` and excluded from the typed method maps (`RequestMethod`/`RequestTypeMap`/`ResultTypeMap`/`NotificationTypeMap`), and `callTool` is typed as plain `CallToolResult`. See docs/migration.md "Wire-only protocol members hidden from the public types". diff --git a/.changeset/spec-corpus-and-leak-net.md b/.changeset/spec-corpus-and-leak-net.md new file mode 100644 index 0000000000..017ecd1501 --- /dev/null +++ b/.changeset/spec-corpus-and-leak-net.md @@ -0,0 +1,5 @@ +--- +'@modelcontextprotocol/core': patch +--- + +Test-only hardening, no runtime changes: a spec example corpus harness (the draft revision's 86 example directories vendored from the specification repository plus a frozen hand-built 2025-11-25 corpus, with rejection-side fixtures routed through real dispatch), a cross-bundle typed-error recognition guard, and extended end-to-end draft-vocabulary leak coverage for hosted transports, SSE streams, and compatibility fallback paths. diff --git a/.changeset/spec-types-2026-repin.md b/.changeset/spec-types-2026-repin.md new file mode 100644 index 0000000000..dbf757cd4e --- /dev/null +++ b/.changeset/spec-types-2026-repin.md @@ -0,0 +1,7 @@ +--- +'@modelcontextprotocol/core': patch +'@modelcontextprotocol/client': patch +'@modelcontextprotocol/server': patch +--- + +Internal: regenerate the 2026-07-28 spec reference types from the latest draft schema (`DiscoverResult` now extends `CacheableResult`; `ElicitationCompleteNotificationParams` extracted as a named interface) and document the anchor lifecycle policy. Released-revision spec-type generation is now pinned to a fixed spec commit; draft anchors keep floating via the nightly refresh PRs. No public API or runtime behavior changes. diff --git a/.github/workflows/conformance.yml b/.github/workflows/conformance.yml index 0deab54482..049b1e8fa0 100644 --- a/.github/workflows/conformance.yml +++ b/.github/workflows/conformance.yml @@ -2,7 +2,7 @@ name: Conformance Tests on: push: - branches: [main] + branches: [main, v2-2026-07-28] pull_request: workflow_dispatch: diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 44852a93d6..5686454414 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -2,6 +2,7 @@ on: push: branches: - main + - v2-2026-07-28 pull_request: workflow_dispatch: diff --git a/.github/workflows/update-spec-types.yml b/.github/workflows/update-spec-types.yml index 482fb04213..41c1303b0a 100644 --- a/.github/workflows/update-spec-types.yml +++ b/.github/workflows/update-spec-types.yml @@ -1,3 +1,14 @@ +# Nightly refresh of the draft-tracking spec anchor (2026-07-28). +# +# Anchor lifecycle (see packages/core/src/types/README.md for the full policy): +# - Draft anchors float: this job regenerates the draft-tracking anchor from the +# latest upstream draft schema and, on drift, opens a refresh PR for review. +# It only ever proposes — it never merges. +# - Released anchors are frozen: generation for released revisions is pinned in +# scripts/fetch-spec-types.ts (RELEASED_REVISION_PINS) and is not refreshed by +# this job. Repinning a released revision — including the freeze of a newly +# published revision, when its schema moves out of schema/draft/ — must land +# in the same commit that retargets this workflow. name: Update Spec Types on: diff --git a/.prettierignore b/.prettierignore index d2fb242b9d..0ece978310 100644 --- a/.prettierignore +++ b/.prettierignore @@ -13,6 +13,14 @@ pnpm-lock.yaml **/src/types/spec.types.2025-11-25.ts **/src/types/spec.types.2026-07-28.ts +# Spec example corpora: vendored verbatim from the spec repository +# (fetch:spec-examples) or hand-built and frozen - byte-faithful artifacts. +packages/core/test/corpus/fixtures/ + +# Schema twins: raw upstream schema.json bytes (fetch:schema-twins), locked to +# manifest.json by sha256 in schemaTwinConformance - reformatting breaks the lock. +packages/core/test/corpus/schema-twins/ + # Batch test cloned repos and results packages/codemod/batch-test/repos packages/codemod/batch-test/results diff --git a/docs/behavior-surface-pins.md b/docs/behavior-surface-pins.md new file mode 100644 index 0000000000..199712e102 --- /dev/null +++ b/docs/behavior-surface-pins.md @@ -0,0 +1,49 @@ +# Behavior-surface pins + +Some tests in this repo are **pins**: they assert the exact current value of a +wire- or consumer-visible behavior — an error code, a schema boundary, an +export map, the stdio env safelist — rather than checking that a feature +works. Their job is to distinguish a deliberate surface change from an +accidental one: the regular suite stays green through either; a pin goes red +through both. + +## When a pin goes red on your change + +A red pin does **not** mean the change is forbidden. It means the change is +surface-visible and must be deliberate: + +1. Confirm the change is intended. If it isn't, the pin just caught an + accidental break. +2. Update the pin in the same PR. +3. Add a changeset if the surface is consumer-facing. +4. Update `docs/migration.md` / `docs/migration-SKILL.md` where consumer-facing. + +Never weaken a pin (loosen an exact match, delete an assertion) just to make +CI pass — that reopens the silent-drift hole the pin exists to close. + +## Where pins live + +| Surface | File | +| --- | --- | +| Wire error-code tables, error classes, version constants | `packages/core/test/types/errorSurfacePins.test.ts` | +| Schema strict/strip/loose boundaries, key existence | `packages/core/test/types/schemaBoundaryPins.test.ts` | +| Published package set, export maps, ESM-only topology | `packages/core/test/packageTopologyPins.test.ts` | +| stdio environment-inheritance safelist | `packages/client/test/client/stdioEnvPins.test.ts` | + +## Writing a new pin + +- The expectation side must be a literal frozen in the test, never a value + imported from src. Comparing a source constant against itself pins nothing. +- Mutation-check it once before landing: flip the source behavior locally and + confirm the pin actually goes red. A pin that stays green under the drift it + claims to guard is worse than no pin. +- Pin behavior a deployed peer or consumer can observe. Internal details that + are invisible across the wire and the public API don't need pins. +- Don't pin a known bug to make it load-bearing — file an issue instead. + +## History + +The original, much broader inventory was developed against v1.x in #2258 and +#2262 (closed unmerged). This sweep ports only the boundary surfaces above; +see those PRs for the fuller exploration and the reasoning behind what was +left out. diff --git a/docs/migration-SKILL.md b/docs/migration-SKILL.md index aef327622c..c906b0bc7d 100644 --- a/docs/migration-SKILL.md +++ b/docs/migration-SKILL.md @@ -501,7 +501,34 @@ The 2025-11 task side-channel through `Protocol` is removed (was always `@experi `TaskStore` / `InMemoryTaskStore` / `CreateTaskOptions` / `isTerminal` (storage layer) are also removed; they will return with the SEP-2663 server-directed plugin. -NOT removed (wire surface, kept for 2025-11-25 interop): task Zod schemas + inferred types (`Task`, `TaskStatus`, `TaskMetadata`, `RelatedTaskMetadata`, `CreateTaskResult`, `GetTask*`, `ListTasks*`, `CancelTask*`, `TaskStatusNotification*`, `TaskAugmentedRequestParams`), task members of the request/result/notification unions, the `tasks` capability key, `isTaskAugmentedRequestParams`, `RELATED_TASK_META_KEY`. Inbound `tasks/*` requests → `-32601`. +NOT removed (wire surface, kept for 2025-11-25 interop, now `@deprecated`): task Zod schemas + inferred types (`Task`, `TaskStatus`, `TaskMetadata`, `RelatedTaskMetadata`, `CreateTaskResult`, `GetTask*`, `ListTasks*`, `CancelTask*`, `TaskStatusNotification*`, `TaskAugmentedRequestParams`), task members of the request/result/notification union types, the `tasks` capability key, `isTaskAugmentedRequestParams`, `RELATED_TASK_META_KEY`. Inbound `tasks/*` requests → `-32601`. + +Task methods are excluded from the typed method maps: `RequestMethod`/`RequestTypeMap`/`ResultTypeMap` have no `tasks/*` entries and `NotificationMethod`/`NotificationTypeMap` have no `notifications/tasks/status`, so the method-keyed overloads of `request()`, `ctx.mcpReq.send()`, `setRequestHandler()`, `setNotificationHandler()` reject task methods at compile time. Mechanical fix where task interop is genuinely required: pass an explicit schema (`request({ method: 'tasks/get', params }, GetTaskResultSchema)`-style custom-method form). `ResultTypeMap['tools/call']` is plain `CallToolResult` (no `| CreateTaskResult`); same for `sampling/createMessage` and `elicitation/create`. + +## 12b. Wire-only members hidden from public types + +`resultType` (2026-07-28 result discrimination) is no longer declared on any public result type; the SDK parses and consumes it internally. The reserved `_meta` envelope keys (`io.modelcontextprotocol/{protocolVersion,clientInfo,clientCapabilities,logLevel}`) and retry fields (`inputResponses`, `requestState`) appear in no public params/result type. `RequestMetaEnvelope` and the `*_META_KEY` constants remain exported. + +| Pattern in v2-alpha code | Mechanical fix | +| ------------------------------------- | --------------------------------------------------------------------------------- | +| `result.resultType` (typed read) | delete the read — the SDK consumes the field; results are complete when delivered | +| `Result['resultType']` type reference | remove; the member is no longer declared | +| return-type capture of `callTool` etc. | use the named public types (`CallToolResult`, `ListToolsResult`, …) | + +Runtime counterpart: inbound reserved envelope keys are lifted out of `params._meta` before handlers run — on requests they are readable at `ctx.mcpReq.envelope` (typed `Partial`, keys present only as received); on notifications there is no ctx, so the lifted envelope keys are dropped and NOT surfaced anywhere. Retry fields (`inputResponses`/`requestState`) lift from REQUEST top-level params only, to `ctx.mcpReq.inputResponses` / `ctx.mcpReq.requestState`; notification params are never touched. On a 2026-era exchange a response carrying a non-`complete` `resultType` rejects with `SdkError` code `UNSUPPORTED_RESULT_TYPE` (kind in `error.data.resultType`), while on a 2025-era connection a foreign `resultType` is stripped before validation; the serving wire era is the instance's negotiated protocol version (connection state), and `MessageExtraInfo.classification` is only validated against it at dispatch (a mismatch is rejected as an entry/routing error). Collision note for 2025-era peers: 2025-11-25 reserves the `io.modelcontextprotocol/` `_meta` prefix but NOT the bare names `inputResponses`/`requestState`, so a 2025 peer's custom-method request using those names as ordinary params has them lifted out of `request.params` (recoverable via ctx; everything else passes through untouched). + +## 12c. Per-era wire codecs (physical deletions + stricter wire schemas) + +The wire layer is split into per-era codecs (2025-era = 2024-10-07 … 2025-11-25; 2026-era = 2026-07-28). Era-mismatched spec methods fail physically: inbound -> `-32601` even with a handler registered; outbound -> `SdkError` code `METHOD_NOT_SUPPORTED_BY_PROTOCOL_VERSION` before the transport. + +| Pattern in v2-alpha code | Mechanical fix | +| ------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------- | +| tool handler returns without `content` | add `content: []` (or real content) — results without it are rejected `-32602`, no longer defaulted | +| parsing wire bytes with `EmptyResultSchema` that may carry `resultType` | strip `resultType` first (the schema now rejects it as an unknown key) | +| strict custom-handler params schema (3-arg `setRequestHandler`/`setNotification…`) | add optional `_meta` to the schema (or strip it) — `_meta` is now passed through minus reserved keys | +| `specTypeSchemas`/`SpecTypeName` references to task message types or `RequestMetaEnvelope` | remove — these validators left the public set (types remain importable) | +| `ClientRequest`/`ServerResult`/… aggregate types expected to include task members | use the individual deprecated `Task*` types — role aggregates are now the neutral (task-free) sets | +| relying on `isCallToolResult` to reject wire-only members | guards validate neutral shapes (loose passthrough); validate raw wire traffic with a transport-level parse | ## 13. Behavioral Changes diff --git a/docs/migration.md b/docs/migration.md index 576f6c5ce4..764203ec2b 100644 --- a/docs/migration.md +++ b/docs/migration.md @@ -902,10 +902,86 @@ The 2025-11 experimental tasks side-channel woven through `Protocol` has been re **Also removed:** the storage layer (`TaskStore`, `InMemoryTaskStore`, `CreateTaskOptions`, `isTerminal`). It will return as part of the SEP-2663 server-directed plugin in a follow-up. -**Wire types remain.** The task wire surface defined by the 2025-11-25 protocol revision is still exported, for interoperability with peers on that revision: the task Zod schemas and their inferred types (`Task`, `TaskStatus`, `TaskMetadata`, `RelatedTaskMetadata`, `CreateTaskResult`, `GetTask*`, `GetTaskPayload*`, `ListTasks*`, `CancelTask*`, `TaskStatusNotification*`, `TaskAugmentedRequestParams`), the task members of the request/result/notification unions, the `tasks` capability key, the `isTaskAugmentedRequestParams` guard, and `RELATED_TASK_META_KEY`. Only the behavior is gone: servers built on this SDK do not advertise the `tasks` capability, and inbound `tasks/*` requests receive a standard `-32601` (method not found) error. +**Wire types remain, as deprecated vocabulary.** The task wire surface defined by the 2025-11-25 protocol revision is still exported, for interoperability with peers on that revision: the task Zod schemas and their inferred types (`Task`, `TaskStatus`, `TaskMetadata`, `RelatedTaskMetadata`, `CreateTaskResult`, `GetTask*`, `GetTaskPayload*`, `ListTasks*`, `CancelTask*`, `TaskStatusNotification*`, `TaskAugmentedRequestParams`), the task members of the request/result/notification union types, the `tasks` capability key, the `isTaskAugmentedRequestParams` guard, and `RELATED_TASK_META_KEY`. These exports are now marked `@deprecated` (importable wire vocabulary only; removable at the major version that drops 2025-era support), and the typed method surface no longer offers task methods: `RequestMethod`/`RequestTypeMap`/`ResultTypeMap`/`NotificationTypeMap` exclude `tasks/*` and `notifications/tasks/status`, so the method-keyed overloads of `request()`, `ctx.mcpReq.send()`, `setRequestHandler()`, and `setNotificationHandler()` do not accept them (the explicit-schema overloads still work for custom interop). The method-keyed result types are narrowed to match: `ResultTypeMap['tools/call']` is plain `CallToolResult` (no `| CreateTaskResult`), and likewise `sampling/createMessage` and `elicitation/create` lose their task-result union members — the runtime result validation uses the same plain schemas, so a task-shaped response body to one of these methods fails as a local `INVALID_RESULT` error where the result schema rejects it rather than parsing into a mis-typed success. Only the behavior is gone: servers built on this SDK do not advertise the `tasks` capability, and inbound `tasks/*` requests receive a standard `-32601` (method not found) error. There is no migration path for the removed surface; it was always `@experimental`. Task support is planned to return as an opt-in extension plugin per SEP-2663. +### Wire-only protocol members hidden from the public types + +The protocol revision 2026-07-28 introduces wire-level bookkeeping that the SDK handles internally and that never needs to reach application code: the `resultType` result discrimination field, the reserved per-request `_meta` envelope keys (`io.modelcontextprotocol/protocolVersion`, `io.modelcontextprotocol/clientInfo`, `io.modelcontextprotocol/clientCapabilities`, `io.modelcontextprotocol/logLevel`), and the multi-round-trip retry fields (`inputResponses`, `requestState`). The public TypeScript surface no longer declares these members: + +- **`resultType` is gone from every public result type** (`Result`, `CallToolResult`, `GetPromptResult`, …, and the `result` member of `JSONRPCResultResponse`). The wire schemas keep parsing it, and the protocol layer consumes it before results reach your code. If you previously read `result.resultType` (it was always `undefined` from conforming 2025-era peers), drop the read — the SDK now owns that field. +- **High-level methods return the named public types.** `client.callTool()` returns `Promise`, `client.listTools()` returns `Promise`, and so on (previously these returned structurally inferred schema types that exposed `resultType?`). Handler return positions are unaffected: results you build keep type-checking, and unknown members still pass through the loose index signature. +- **The reserved envelope keys and retry fields never appear in a public params/result type.** The `RequestMetaEnvelope` type and the four `*_META_KEY` constants stay exported — they document the wire names and type the context surfacing channel (see below). + +The protocol layer enforces the same boundary at runtime: + +- **Envelope lift.** On inbound requests and notifications, the reserved `io.modelcontextprotocol/*` envelope keys are lifted out of `params._meta` before handlers run, so handler params are byte-equal to the 2025-era shape under 2026-era traffic. For requests the envelope is readable at `ctx.mcpReq.envelope` (typed `Partial` — only the keys the request actually carried are present); for notifications there is no per-message context, so lifted envelope keys are dropped, not surfaced. On requests only, the multi-round-trip retry fields are likewise lifted out of top-level params and surfaced verbatim at `ctx.mcpReq.inputResponses` / `ctx.mcpReq.requestState`; notification params are never touched. +- **What this means for 2025-era peers.** The `_meta` side of the lift is invisible to conforming 2025-era traffic: the `io.modelcontextprotocol/` prefix is reserved in 2025-11-25 too, so a conforming 2025 peer never puts application data under those keys. The retry-field lift is the one collision to know about: 2025-11-25 does not reserve the bare names `inputResponses`/`requestState`, so a 2025 peer's **custom-method request** that happens to use them as ordinary top-level params will have them lifted out of the handler's view (still readable at `ctx.mcpReq.inputResponses` / `ctx.mcpReq.requestState`, just no longer in `request.params`). Spec-method requests are unaffected (no 2025 spec method defines params with those names), as are all notifications. +- **Raw-first result discrimination.** The client funnel inspects a response's raw `resultType` before schema validation: `'complete'` is consumed (stripped) and the result parses as the public shape; any other kind (e.g. `input_required`) rejects with a typed local error — `SdkError` with the new code `SdkErrorCode.UnsupportedResultType` and the kind in `error.data.resultType` — instead of being masked into a hollow success by tolerant result schemas. Full multi-round-trip support will replace that error arm. +- **`MessageExtraInfo.classification`** is an optional carrier (`{ era, revision?, envelope? }`) for transports that classify inbound messages at the edge. The wire era itself is connection state (the negotiated protocol version held by the `Client`/`Server` instance); dispatch validates a classified message against that era and treats a mismatch as an entry/routing error (see the next section). + +**Before (v2 alpha):** + +```typescript +const result = await client.callTool({ name: 'echo', arguments: {} }); +// result.resultType was declared as `string | undefined` and always undefined +if (result.resultType === undefined || result.resultType === 'complete') { + console.log(result.content); +} +``` + +**After:** + +```typescript +const result = await client.callTool({ name: 'echo', arguments: {} }); +// resultType is wire-level bookkeeping the SDK consumes; just use the result +console.log(result.content); +``` + +### Per-era wire codecs: physical deletions and stricter wire schemas + +The wire layer is now split into per-revision codecs inside the (private, bundled) core: one codec serves every 2025-era protocol version (2024-10-07 … 2025-11-25) and one serves 2026-07-28. The codec is selected by the negotiated protocol version, which is connection state on the `Client`/`Server` instance: the client stores it when its initialize handshake completes, the server stores it when it answers `initialize`, and instances with no negotiated version default to the 2025 era (with the pre-negotiation lifecycle messages routed by method: `initialize`/`notifications/initialized` are 2025-era vocabulary, `server/discover` is 2026-era vocabulary). An edge classification (`MessageExtraInfo.classification`) no longer switches the era per message — it is validated against the instance era, and a mismatch is rejected as an entry/routing error (`-32004 Unsupported protocol version` for requests, a drop plus `onerror` for notifications). Methods deleted by a protocol revision are now PHYSICALLY absent from that era's registry: an inbound `tasks/get` on a 2026-era connection gets `-32601` even if a handler is registered, and sending an era-mismatched spec method (for example `server/discover` toward a 2025-era peer, or any `tasks/*` method toward a 2026-era peer) throws a typed local error — `SdkError` with the new code `SdkErrorCode.MethodNotSupportedByProtocolVersion` — before anything reaches the transport. + +Alongside the split, the following deliberate wire-behavior changes ship (each is invisible to conforming peers but observable to direct schema consumers and misbehaving peers): + +- **`resultType` is no longer modeled by any neutral wire schema.** The base `ResultSchema` (and every result schema derived from it) no longer declares the optional `resultType` member. Consequences: + - `EmptyResultSchema` (strict) now REJECTS `{resultType: ...}` bodies where it previously accepted them. On the protocol path nothing changes for conforming peers: the 2026-era codec consumes the field, and the 2025-era codec strips a foreign `resultType` before validation (tolerate-and-drop — a 2025-era peer that sends it is misbehaving). + - On a 2025-era connection, a response carrying a non-`'complete'` `resultType` is no longer rejected with `UnsupportedResultType`: the field is foreign vocabulary on that era and is stripped before validation (the result then passes or fails validation on its actual content, loudly). On a 2026-era exchange the discrimination is stricter than before: `resultType` is REQUIRED, an absent value is a spec violation surfaced as a typed error, and `input_required` / unknown kinds reject with `UnsupportedResultType` / `InvalidResult`. +- **`CallToolResult.content` and `ToolResultContent.content` are required at the wire boundary.** The `content.default([])` affordance was removed (it could silently convert unrecognized result shapes into hollow `{content: []}` successes). Tool handlers MUST include `content` in their results (the TypeScript surface always required it — `content: []` is fine); a handler result without it is now rejected with `-32602 Invalid tools/call result` instead of being silently defaulted, and a content-less wire result fails the client-side parse loudly. +- **Custom (3-arg) handlers receive `_meta`.** `setRequestHandler(method, {params}, handler)` / `setNotificationHandler(method, {params}, handler)` used to DELETE `params._meta` before validating with your schema. They now pass it through minus the reserved `io.modelcontextprotocol/*` envelope keys (which the protocol layer lifts out), making custom methods consistent with spec methods. If your params schema is strict (rejects unknown keys), add an optional `_meta` member or strip it yourself. +- **`specTypeSchemas` validate the neutral model.** Result entries no longer accept/declare `resultType`; the validators for the 2025-only task message types (`Task`, `TaskStatus`, `GetTask*`, `ListTasks*`, `CancelTask*`, `CreateTaskResult`, `TaskStatusNotification*`, `TaskCreationParams`) and for `RequestMetaEnvelope` left the public set (`SpecTypeName` narrowed accordingly). Per-revision wire validators are planned to return as versioned `zod-schemas/` exports. +- **Role aggregate types no longer carry task vocabulary.** `ClientRequest`, `ClientResult`, `ClientNotification`, `ServerRequest`, `ServerResult`, and `ServerNotification` (and their union schemas) are now the neutral message sets; the task members moved into the internal 2025-era wire module. The individual `Task*` types remain importable (deprecated) exactly as before. +- **Value guards are consumer-side checks, not wire validators.** `isCallToolResult` and friends now validate the neutral shapes; a raw wire object carrying `resultType` still passes them through the loose index signature. Validate raw wire traffic with a transport-level parse, not the guards. + +**Before:** + +```typescript +// A handler omitting content was silently defaulted on the wire: +server.setRequestHandler('tools/call', async () => { + return { structuredContent: { ok: true } } as CallToolResult; // wire: content [] +}); + +// Custom handlers never saw _meta: +protocol.setRequestHandler('acme/op', { params: z.strictObject({ x: z.number() }) }, async params => ({})); +``` + +**After:** + +```typescript +// content is required (as the spec always said): +server.setRequestHandler('tools/call', async () => { + return { content: [], structuredContent: { ok: true } }; +}); + +// Custom handlers receive _meta minus the reserved envelope keys: +protocol.setRequestHandler( + 'acme/op', + { params: z.strictObject({ x: z.number(), _meta: z.record(z.string(), z.unknown()).optional() }) }, + async params => ({}) +); +``` + ## Enhancements ### Automatic JSON Schema validator selection by runtime diff --git a/package.json b/package.json index d1ecc0c627..03c4132988 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,8 @@ "mcp" ], "scripts": { + "fetch:schema-twins": "tsx scripts/fetch-schema-twins.ts", + "fetch:spec-examples": "tsx scripts/fetch-spec-examples.ts", "fetch:spec-types": "tsx scripts/fetch-spec-types.ts", "sync:snippets": "tsx scripts/sync-snippets.ts", "examples:simple-server:w": "pnpm --filter @modelcontextprotocol/examples-server exec tsx --watch src/simpleStreamableHttp.ts --oauth", @@ -46,14 +48,13 @@ "test:conformance:all": "pnpm run test:conformance:client:all && pnpm run test:conformance:server:all" }, "devDependencies": { - "lefthook": "^2.0.16", "@cfworker/json-schema": "catalog:runtimeShared", "@changesets/changelog-github": "^0.5.2", "@changesets/cli": "^2.29.8", "@eslint/js": "catalog:devTools", "@modelcontextprotocol/client": "workspace:^", - "@modelcontextprotocol/server": "workspace:^", "@modelcontextprotocol/node": "workspace:^", + "@modelcontextprotocol/server": "workspace:^", "@types/content-type": "catalog:devTools", "@types/cors": "catalog:devTools", "@types/cross-spawn": "catalog:devTools", @@ -67,6 +68,7 @@ "eslint-config-prettier": "catalog:devTools", "eslint-plugin-n": "catalog:devTools", "fast-glob": "^3.3.3", + "lefthook": "^2.0.16", "prettier": "catalog:devTools", "supertest": "catalog:devTools", "tsdown": "catalog:devTools", diff --git a/packages/client/src/client/client.ts b/packages/client/src/client/client.ts index bc3a91150b..29710cbea4 100644 --- a/packages/client/src/client/client.ts +++ b/packages/client/src/client/client.ts @@ -2,12 +2,16 @@ import { DefaultJsonSchemaValidator } from '@modelcontextprotocol/client/_shims' import type { BaseContext, CallToolRequest, + CallToolResult, ClientCapabilities, ClientContext, ClientNotification, ClientRequest, CompleteRequest, + CompleteResult, + EmptyResult, GetPromptRequest, + GetPromptResult, Implementation, JSONRPCRequest, JsonSchemaType, @@ -16,14 +20,19 @@ import type { ListChangedHandlers, ListChangedOptions, ListPromptsRequest, + ListPromptsResult, ListResourcesRequest, + ListResourcesResult, ListResourceTemplatesRequest, + ListResourceTemplatesResult, ListToolsRequest, + ListToolsResult, LoggingLevel, MessageExtraInfo, NotificationMethod, ProtocolOptions, ReadResourceRequest, + ReadResourceResult, RequestMethod, RequestOptions, Result, @@ -34,30 +43,20 @@ import type { UnsubscribeRequest } from '@modelcontextprotocol/core'; import { - CallToolResultSchema, - CompleteResultSchema, - CreateMessageRequestSchema, + codecForVersion, CreateMessageResultSchema, CreateMessageResultWithToolsSchema, - ElicitRequestSchema, - ElicitResultSchema, - EmptyResultSchema, - GetPromptResultSchema, - InitializeResultSchema, LATEST_PROTOCOL_VERSION, ListChangedOptionsBaseSchema, - ListPromptsResultSchema, - ListResourcesResultSchema, - ListResourceTemplatesResultSchema, - ListToolsResultSchema, mergeCapabilities, + negotiatedProtocolVersionOf, parseSchema, Protocol, ProtocolError, ProtocolErrorCode, - ReadResourceResultSchema, SdkError, - SdkErrorCode + SdkErrorCode, + setNegotiatedProtocolVersion } from '@modelcontextprotocol/core'; /** @@ -216,7 +215,6 @@ export type ClientOptions = ProtocolOptions & { export class Client extends Protocol { private _serverCapabilities?: ServerCapabilities; private _serverVersion?: Implementation; - private _negotiatedProtocolVersion?: string; private _capabilities: ClientCapabilities; private _instructions?: string; private _jsonSchemaValidator: jsonSchemaValidator; @@ -299,7 +297,19 @@ export class Client extends Protocol { ): (request: JSONRPCRequest, ctx: ClientContext) => Promise { if (method === 'elicitation/create') { return async (request, ctx) => { - const validatedRequest = parseSchema(ElicitRequestSchema, request); + // Era-exact validation: the schemas are resolved from the + // instance era at dispatch time (the era gate guarantees the + // method exists on the serving era before we get here). + const codec = codecForVersion(negotiatedProtocolVersionOf(this)); + const elicitRequestSchema = codec.requestSchema('elicitation/create'); + // The era registry entry IS the plain ElicitResult schema + // (the result map is aligned to the typed map — no widened + // unions), so no narrower surface is needed. + const elicitResultSchema = codec.resultSchema('elicitation/create'); + if (!elicitRequestSchema || !elicitResultSchema) { + throw new ProtocolError(ProtocolErrorCode.InternalError, 'No wire schema for elicitation/create in the resolved era'); + } + const validatedRequest = parseSchema(elicitRequestSchema, request); if (!validatedRequest.success) { // Type guard: if success is false, error is guaranteed to exist const errorMessage = @@ -321,7 +331,7 @@ export class Client extends Protocol { const result = await handler(request, ctx); - const validationResult = parseSchema(ElicitResultSchema, result); + const validationResult = parseSchema(elicitResultSchema, result); if (!validationResult.success) { // Type guard: if success is false, error is guaranteed to exist const errorMessage = @@ -352,7 +362,16 @@ export class Client extends Protocol { if (method === 'sampling/createMessage') { return async (request, ctx) => { - const validatedRequest = parseSchema(CreateMessageRequestSchema, request); + // Era-exact validation via the instance era (see above). + const codec = codecForVersion(negotiatedProtocolVersionOf(this)); + const samplingRequestSchema = codec.requestSchema('sampling/createMessage'); + if (!samplingRequestSchema) { + throw new ProtocolError( + ProtocolErrorCode.InternalError, + 'No wire schema for sampling/createMessage in the resolved era' + ); + } + const validatedRequest = parseSchema(samplingRequestSchema, request); if (!validatedRequest.success) { const errorMessage = validatedRequest.error instanceof Error ? validatedRequest.error.message : String(validatedRequest.error); @@ -363,6 +382,11 @@ export class Client extends Protocol { const result = await handler(request, ctx); + // The result schema depends on the REQUEST params (tools vs + // no tools) — something a method-keyed registry entry cannot + // express, so the pair is picked here. The era gate keeps + // this era-correct: sampling/createMessage is only ever + // dispatched on an era whose registry defines it. const hasTools = params.tools || params.toolChoice; const resultSchema = hasTools ? CreateMessageResultWithToolsSchema : CreateMessageResultSchema; const validationResult = parseSchema(resultSchema, result); @@ -420,13 +444,26 @@ export class Client extends Protocol { // Restore the protocol version negotiated during the original initialize handshake // so HTTP transports include the required mcp-protocol-version header, but skip re-init. if (transport.sessionId !== undefined) { - if (this._negotiatedProtocolVersion !== undefined && transport.setProtocolVersion) { - transport.setProtocolVersion(this._negotiatedProtocolVersion); + const negotiatedProtocolVersion = negotiatedProtocolVersionOf(this); + if (negotiatedProtocolVersion !== undefined) { + // Resuming keeps the original negotiation: the instance still + // holds the negotiated version (and with it the wire era) — + // only the new transport needs the header pushed again. + transport.setProtocolVersion?.(negotiatedProtocolVersion); } return; } + // Fresh connect: the negotiated protocol version is connection state — + // a value left over from a previous connection must not survive into a + // new handshake. Clearing it puts the instance back in the + // pre-negotiation phase, so the initialize exchange below rides the + // bootstrap method pins (legacy era) instead of a dead session's era. + // Without this, an instance that once negotiated a modern era could + // never re-run a fresh handshake: `initialize` is physically absent + // from the modern registry. (The resume branch above keeps it instead.) + setNegotiatedProtocolVersion(this, undefined); try { - const result = await this._requestWithSchema( + const result = await this.request( { method: 'initialize', params: { @@ -435,7 +472,6 @@ export class Client extends Protocol { clientInfo: this._clientInfo } }, - InitializeResultSchema, options ); @@ -449,7 +485,6 @@ export class Client extends Protocol { this._serverCapabilities = result.capabilities; this._serverVersion = result.serverInfo; - this._negotiatedProtocolVersion = result.protocolVersion; // HTTP transports must set the protocol version in each header after initialization. if (transport.setProtocolVersion) { transport.setProtocolVersion(result.protocolVersion); @@ -461,6 +496,15 @@ export class Client extends Protocol { method: 'notifications/initialized' }); + // Handshake completion: the negotiated version becomes the + // instance's connection state, and with it the wire era for + // everything this connection sends/receives from here on (the + // negotiated version cashes out as the negotiated wire ERA — + // Q1-SD1). Set AFTER the initialized notification: the initialize + // EXCHANGE is the legacy handshake by definition and completes on + // that era. + setNegotiatedProtocolVersion(this, result.protocolVersion); + // Set up list changed handlers now that we know server capabilities if (this._pendingListChangedConfig) { this._setupListChangedHandlers(this._pendingListChangedConfig); @@ -493,7 +537,7 @@ export class Client extends Protocol { * value to the new transport so it continues sending the required `mcp-protocol-version` header. */ getNegotiatedProtocolVersion(): string | undefined { - return this._negotiatedProtocolVersion; + return negotiatedProtocolVersionOf(this); } /** @@ -642,13 +686,13 @@ export class Client extends Protocol { } } - async ping(options?: RequestOptions) { - return this._requestWithSchema({ method: 'ping' }, EmptyResultSchema, options); + async ping(options?: RequestOptions): Promise { + return this.request({ method: 'ping' }, options); } /** Requests argument autocompletion suggestions from the server for a prompt or resource. */ - async complete(params: CompleteRequest['params'], options?: RequestOptions) { - return this._requestWithSchema({ method: 'completion/complete', params }, CompleteResultSchema, options); + async complete(params: CompleteRequest['params'], options?: RequestOptions): Promise { + return this.request({ method: 'completion/complete', params }, options); } /** @@ -658,13 +702,13 @@ export class Client extends Protocol { * Remains functional during the deprecation window (at least twelve months). * Migrate to stderr logging (STDIO servers) or OpenTelemetry. */ - async setLoggingLevel(level: LoggingLevel, options?: RequestOptions) { - return this._requestWithSchema({ method: 'logging/setLevel', params: { level } }, EmptyResultSchema, options); + async setLoggingLevel(level: LoggingLevel, options?: RequestOptions): Promise { + return this.request({ method: 'logging/setLevel', params: { level } }, options); } /** Retrieves a prompt by name from the server, passing the given arguments for template substitution. */ - async getPrompt(params: GetPromptRequest['params'], options?: RequestOptions) { - return this._requestWithSchema({ method: 'prompts/get', params }, GetPromptResultSchema, options); + async getPrompt(params: GetPromptRequest['params'], options?: RequestOptions): Promise { + return this.request({ method: 'prompts/get', params }, options); } /** @@ -689,13 +733,13 @@ export class Client extends Protocol { * ); * ``` */ - async listPrompts(params?: ListPromptsRequest['params'], options?: RequestOptions) { + async listPrompts(params?: ListPromptsRequest['params'], options?: RequestOptions): Promise { if (!this._serverCapabilities?.prompts && !this._enforceStrictCapabilities) { // Respect capability negotiation: server does not support prompts console.debug('Client.listPrompts() called but server does not advertise prompts capability - returning empty list'); return { prompts: [] }; } - return this._requestWithSchema({ method: 'prompts/list', params }, ListPromptsResultSchema, options); + return this.request({ method: 'prompts/list', params }, options); } /** @@ -720,13 +764,13 @@ export class Client extends Protocol { * ); * ``` */ - async listResources(params?: ListResourcesRequest['params'], options?: RequestOptions) { + async listResources(params?: ListResourcesRequest['params'], options?: RequestOptions): Promise { if (!this._serverCapabilities?.resources && !this._enforceStrictCapabilities) { // Respect capability negotiation: server does not support resources console.debug('Client.listResources() called but server does not advertise resources capability - returning empty list'); return { resources: [] }; } - return this._requestWithSchema({ method: 'resources/list', params }, ListResourcesResultSchema, options); + return this.request({ method: 'resources/list', params }, options); } /** @@ -735,7 +779,10 @@ export class Client extends Protocol { * Returns an empty list if the server does not advertise resources capability * (or throws if {@linkcode ClientOptions.enforceStrictCapabilities} is enabled). */ - async listResourceTemplates(params?: ListResourceTemplatesRequest['params'], options?: RequestOptions) { + async listResourceTemplates( + params?: ListResourceTemplatesRequest['params'], + options?: RequestOptions + ): Promise { if (!this._serverCapabilities?.resources && !this._enforceStrictCapabilities) { // Respect capability negotiation: server does not support resources console.debug( @@ -743,22 +790,22 @@ export class Client extends Protocol { ); return { resourceTemplates: [] }; } - return this._requestWithSchema({ method: 'resources/templates/list', params }, ListResourceTemplatesResultSchema, options); + return this.request({ method: 'resources/templates/list', params }, options); } /** Reads the contents of a resource by URI. */ - async readResource(params: ReadResourceRequest['params'], options?: RequestOptions) { - return this._requestWithSchema({ method: 'resources/read', params }, ReadResourceResultSchema, options); + async readResource(params: ReadResourceRequest['params'], options?: RequestOptions): Promise { + return this.request({ method: 'resources/read', params }, options); } /** Subscribes to change notifications for a resource. The server must support resource subscriptions. */ - async subscribeResource(params: SubscribeRequest['params'], options?: RequestOptions) { - return this._requestWithSchema({ method: 'resources/subscribe', params }, EmptyResultSchema, options); + async subscribeResource(params: SubscribeRequest['params'], options?: RequestOptions): Promise { + return this.request({ method: 'resources/subscribe', params }, options); } /** Unsubscribes from change notifications for a resource. */ - async unsubscribeResource(params: UnsubscribeRequest['params'], options?: RequestOptions) { - return this._requestWithSchema({ method: 'resources/unsubscribe', params }, EmptyResultSchema, options); + async unsubscribeResource(params: UnsubscribeRequest['params'], options?: RequestOptions): Promise { + return this.request({ method: 'resources/unsubscribe', params }, options); } /** @@ -798,8 +845,12 @@ export class Client extends Protocol { * } * ``` */ - async callTool(params: CallToolRequest['params'], options?: RequestOptions) { - const result = await this._requestWithSchema({ method: 'tools/call', params }, CallToolResultSchema, options); + async callTool(params: CallToolRequest['params'], options?: RequestOptions): Promise { + // The method-keyed request() path validates the era registry's plain + // CallToolResult schema — with the result map aligned to the typed + // map there is no wider union to narrow away (Q1-SD2 holds by + // construction). + const result = await this.request({ method: 'tools/call', params }, options); // Check if the tool has an outputSchema const validator = this.getToolOutputValidator(params.name); @@ -884,13 +935,13 @@ export class Client extends Protocol { * ); * ``` */ - async listTools(params?: ListToolsRequest['params'], options?: RequestOptions) { + async listTools(params?: ListToolsRequest['params'], options?: RequestOptions): Promise { if (!this._serverCapabilities?.tools && !this._enforceStrictCapabilities) { // Respect capability negotiation: server does not support tools console.debug('Client.listTools() called but server does not advertise tools capability - returning empty list'); return { tools: [] }; } - const result = await this._requestWithSchema({ method: 'tools/list', params }, ListToolsResultSchema, options); + const result = await this.request({ method: 'tools/list', params }, options); // Cache the tools and their output schemas for future validation this.cacheToolMetadata(result.tools); diff --git a/packages/client/test/client/clientTypeSurface.test.ts b/packages/client/test/client/clientTypeSurface.test.ts new file mode 100644 index 0000000000..c6246a8fed --- /dev/null +++ b/packages/client/test/client/clientTypeSurface.test.ts @@ -0,0 +1,30 @@ +/** + * Type-surface pins for the client's high-level methods. + * + * `callTool` returns plain `CallToolResult` on every protocol era — no task + * union (a v2 client never sends a task-augmented call, so a task result is + * unreachable from its API) and no wire-only members (`resultType` is + * consumed at the protocol layer and never reaches consumers). + */ +import type { CallToolResult, EmptyResult, ListToolsResult, ReadResourceResult } from '@modelcontextprotocol/core'; +import { describe, expectTypeOf, test } from 'vitest'; + +import { Client } from '../../src/client/client.js'; + +type KnownKeyOf = keyof { [K in keyof T as string extends K ? never : number extends K ? never : K]: T[K] }; + +describe('client method return types', () => { + test('callTool returns plain CallToolResult (no union, no wire-only members)', () => { + type Return = Awaited>; + expectTypeOf().toEqualTypeOf(); + expectTypeOf, 'resultType'>>().toEqualTypeOf(); + expectTypeOf, 'task'>>().toEqualTypeOf(); + }); + + test('the other request methods return the public result types', () => { + expectTypeOf>>().toEqualTypeOf(); + expectTypeOf>>().toEqualTypeOf(); + expectTypeOf>>().toEqualTypeOf(); + expectTypeOf>>, 'resultType'>>().toEqualTypeOf(); + }); +}); diff --git a/packages/client/test/client/stdioEnvPins.test.ts b/packages/client/test/client/stdioEnvPins.test.ts new file mode 100644 index 0000000000..35d6d8747d --- /dev/null +++ b/packages/client/test/client/stdioEnvPins.test.ts @@ -0,0 +1,69 @@ +/** + * Behavior-surface pins: the stdio environment-inheritance safelist. + * + * getDefaultEnvironment() decides which parent environment variables every + * spawned stdio server inherits. Widening the safelist leaks more of the + * parent environment into child processes, so both the list itself and the + * filtering behavior are pinned. A failing pin here means the change is + * deliberate: update the pin in the same change, together with a changeset + * and a migration-doc entry. + * + * See docs/behavior-surface-pins.md for the maintenance protocol. + */ +import { afterEach, describe, expect, test, vi } from 'vitest'; + +import { DEFAULT_INHERITED_ENV_VARS, getDefaultEnvironment } from '../../src/client/stdio.js'; + +// Frozen copy of the documented safelist. The expectation side is a literal, +// not derived from src, so any edit to DEFAULT_INHERITED_ENV_VARS goes red +// here regardless of which variables happen to be set in the runner's +// environment. (The behavioral test below cannot catch a widened safelist on +// its own: getDefaultEnvironment skips unset keys, and sensitive variables +// are exactly the ones typically unset in CI.) +const SAFELIST = + process.platform === 'win32' + ? [ + 'APPDATA', + 'HOMEDRIVE', + 'HOMEPATH', + 'LOCALAPPDATA', + 'PATH', + 'PROCESSOR_ARCHITECTURE', + 'SYSTEMDRIVE', + 'SYSTEMROOT', + 'TEMP', + 'USERNAME', + 'USERPROFILE', + 'PROGRAMFILES' + ] + : ['HOME', 'LOGNAME', 'PATH', 'SHELL', 'TERM', 'USER']; + +describe('stdio environment safelist', () => { + afterEach(() => { + vi.unstubAllEnvs(); + }); + + test('DEFAULT_INHERITED_ENV_VARS matches the frozen safelist exactly', () => { + expect([...DEFAULT_INHERITED_ENV_VARS].sort()).toEqual([...SAFELIST].sort()); + }); + + test('getDefaultEnvironment inherits exactly the safelist keys that are set', () => { + for (const key of SAFELIST) { + vi.stubEnv(key, `safe-${key}`); + } + vi.stubEnv('STDIO_PIN_SECRET', 'must-not-be-inherited'); + + const env = getDefaultEnvironment(); + + expect(Object.keys(env).sort()).toEqual([...SAFELIST].sort()); + for (const key of SAFELIST) { + expect(env[key]).toBe(`safe-${key}`); + } + }); + + test('skips values that look like exported shell functions', () => { + vi.stubEnv('PATH', '() { echo pwned; }'); + const env = getDefaultEnvironment(); + expect(env.PATH).toBeUndefined(); + }); +}); diff --git a/packages/codemod/src/generated/specSchemaMap.ts b/packages/codemod/src/generated/specSchemaMap.ts index 77f3d3dfc8..99d8f84dfb 100644 --- a/packages/codemod/src/generated/specSchemaMap.ts +++ b/packages/codemod/src/generated/specSchemaMap.ts @@ -8,8 +8,6 @@ export const SPEC_SCHEMA_NAMES: ReadonlySet = new Set([ 'CallToolRequestParamsSchema', 'CallToolRequestSchema', 'CallToolResultSchema', - 'CancelTaskRequestSchema', - 'CancelTaskResultSchema', 'CancelledNotificationParamsSchema', 'CancelledNotificationSchema', 'ClientCapabilitiesSchema', @@ -25,7 +23,6 @@ export const SPEC_SCHEMA_NAMES: ReadonlySet = new Set([ 'CreateMessageRequestSchema', 'CreateMessageResultSchema', 'CreateMessageResultWithToolsSchema', - 'CreateTaskResultSchema', 'CursorSchema', 'DiscoverRequestSchema', 'DiscoverResultSchema', @@ -42,10 +39,6 @@ export const SPEC_SCHEMA_NAMES: ReadonlySet = new Set([ 'GetPromptRequestParamsSchema', 'GetPromptRequestSchema', 'GetPromptResultSchema', - 'GetTaskPayloadRequestSchema', - 'GetTaskPayloadResultSchema', - 'GetTaskRequestSchema', - 'GetTaskResultSchema', 'IconSchema', 'IconsSchema', 'ImageContentSchema', @@ -72,8 +65,6 @@ export const SPEC_SCHEMA_NAMES: ReadonlySet = new Set([ 'ListResourcesResultSchema', 'ListRootsRequestSchema', 'ListRootsResultSchema', - 'ListTasksRequestSchema', - 'ListTasksResultSchema', 'ListToolsRequestSchema', 'ListToolsResultSchema', 'LoggingLevelSchema', @@ -114,7 +105,6 @@ export const SPEC_SCHEMA_NAMES: ReadonlySet = new Set([ 'ReadResourceResultSchema', 'RelatedTaskMetadataSchema', 'RequestIdSchema', - 'RequestMetaEnvelopeSchema', 'RequestMetaSchema', 'RequestSchema', 'ResourceContentsSchema', @@ -144,12 +134,7 @@ export const SPEC_SCHEMA_NAMES: ReadonlySet = new Set([ 'SubscribeRequestParamsSchema', 'SubscribeRequestSchema', 'TaskAugmentedRequestParamsSchema', - 'TaskCreationParamsSchema', 'TaskMetadataSchema', - 'TaskSchema', - 'TaskStatusNotificationParamsSchema', - 'TaskStatusNotificationSchema', - 'TaskStatusSchema', 'TextContentSchema', 'TextResourceContentsSchema', 'TitledMultiSelectEnumSchemaSchema', diff --git a/packages/codemod/src/migrations/v1-to-v2/transforms/mcpServerApi.ts b/packages/codemod/src/migrations/v1-to-v2/transforms/mcpServerApi.ts index 9efc2d5839..706c1e6e66 100644 --- a/packages/codemod/src/migrations/v1-to-v2/transforms/mcpServerApi.ts +++ b/packages/codemod/src/migrations/v1-to-v2/transforms/mcpServerApi.ts @@ -121,7 +121,7 @@ export const mcpServerApiTransform: Transform = { } } - changesCount += migrateConstructorTaskOptions(sourceFile, diagnostics); + flagRemovedTaskOptions(sourceFile, diagnostics); return { changesCount, diagnostics }; } @@ -414,11 +414,17 @@ function migrateResourceCall(call: CallExpression, _sourceFile: SourceFile): boo const TASK_OPTIONS = ['taskStore', 'taskMessageQueue'] as const; -function migrateConstructorTaskOptions(sourceFile: SourceFile, diagnostics: Diagnostic[]): number { +/** + * Flag v1 task runtime options on the McpServer constructor as removed. + * + * The experimental tasks runtime was removed in v2 (SEP-2663) with no replacement, so + * these options cannot be migrated automatically. Emit an action-required diagnostic + * matching the importMap removal entry for `experimental/tasks`; the source is left + * untouched. + */ +function flagRemovedTaskOptions(sourceFile: SourceFile, diagnostics: Diagnostic[]): void { const localName = resolveLocalImportName(sourceFile, 'McpServer'); - if (!localName) return 0; - - let changes = 0; + if (!localName) return; for (const node of sourceFile.getDescendantsOfKind(SyntaxKind.NewExpression)) { if (node.wasForgotten()) continue; @@ -431,110 +437,15 @@ function migrateConstructorTaskOptions(sourceFile: SourceFile, diagnostics: Diag const optionsArg = args[1]!; if (!Node.isObjectLiteralExpression(optionsArg)) continue; - // Check if any task options are present at the top level - const propsToMove: string[] = []; for (const propName of TASK_OPTIONS) { - if (optionsArg.getProperty(propName)) { - propsToMove.push(propName); - } - } - if (propsToMove.length === 0) continue; - - // Find the tasks object's position within the options text using AST, - // then do all mutations via a single text replacement to avoid node invalidation. - const capabilitiesProp = optionsArg.getProperty('capabilities'); - let tasksObjStart = -1; - let tasksObjEnd = -1; - const optionsStart = optionsArg.getStart(); - if (capabilitiesProp && Node.isPropertyAssignment(capabilitiesProp)) { - const capInit = capabilitiesProp.getInitializer(); - if (capInit && Node.isObjectLiteralExpression(capInit)) { - const tasksProp = capInit.getProperty('tasks'); - if (tasksProp && Node.isPropertyAssignment(tasksProp)) { - const tasksInit = tasksProp.getInitializer(); - if (tasksInit && Node.isObjectLiteralExpression(tasksInit)) { - tasksObjStart = tasksInit.getStart() - optionsStart; - tasksObjEnd = tasksInit.getEnd() - optionsStart; - } - } - } - } - - if (tasksObjStart === -1) { - for (const propName of propsToMove) { - diagnostics.push( - actionRequired( - sourceFile.getFilePath(), - node, - `Move '${propName}' from McpServer options into capabilities.tasks — v2 expects task runtime options inside the tasks capability.` - ) - ); - } - continue; - } - - // Single text replacement: remove top-level props and insert into tasks object. - // Use AST nodes (already located via getProperty) to get brace-balanced text and - // exact positions, avoiding regex truncation on values containing commas/braces. - // Collect all properties first, then process in reverse position order so each - // removal doesn't invalidate the positions of subsequent removals. - let optionsText = optionsArg.getText(); - const argStart = optionsArg.getStart(); - const propsWithPositions: { text: string; start: number; end: number }[] = []; - for (const propName of propsToMove) { - const prop = optionsArg.getProperty(propName); - if (!prop) continue; - propsWithPositions.push({ - text: prop.getText(), - start: prop.getStart() - argStart, - end: prop.getEnd() - argStart - }); + if (!optionsArg.getProperty(propName)) continue; + diagnostics.push( + actionRequired( + sourceFile.getFilePath(), + node, + `Remove '${propName}' from McpServer options — experimental tasks removed in v2 (SEP-2663 — tasks moved to the Extensions Track). No v2 equivalent.` + ) + ); } - const propTexts = propsWithPositions.map(p => p.text); - - // Remove in reverse position order so earlier positions remain valid - const sortedProps = propsWithPositions.toSorted((a, b) => b.start - a.start); - for (const { start, end } of sortedProps) { - let remStart = start; - let remEnd = end; - // Consume trailing comma and whitespace - const afterProp = optionsText.slice(remEnd); - const trailingMatch = afterProp.match(/^\s*,?\s*/); - if (trailingMatch) { - remEnd += trailingMatch[0].length; - } - // Consume leading whitespace/newline - const beforeProp = optionsText.slice(0, remStart); - const leadingMatch = beforeProp.match(/[\n\r]?\s*$/); - if (leadingMatch) { - remStart -= leadingMatch[0].length; - } - optionsText = optionsText.slice(0, remStart) + optionsText.slice(remEnd); - // Adjust tasks position if removal was before it - if (remStart < tasksObjStart) { - const shift = remEnd - remStart; - tasksObjStart -= shift; - tasksObjEnd -= shift; - } - } - - if (propTexts.length === 0) continue; - - // Insert into the tasks object (just before its closing brace) - const tasksText = optionsText.slice(tasksObjStart, tasksObjEnd); - const closingBrace = tasksText.lastIndexOf('}'); - const before = tasksText.slice(0, closingBrace).trimEnd(); - const sep = before.length > 1 ? ',\n' : '\n'; - const newTasksText = before + sep + propTexts.join(',\n') + '\n' + tasksText.slice(closingBrace); - optionsText = optionsText.slice(0, tasksObjStart) + newTasksText + optionsText.slice(tasksObjEnd); - - // Clean up double/trailing commas - optionsText = optionsText.replaceAll(/,(\s*,)/g, ','); - optionsText = optionsText.replaceAll(/,(\s*})/g, '$1'); - - optionsArg.replaceWithText(optionsText); - changes += propTexts.length; } - - return changes; } diff --git a/packages/codemod/test/v1-to-v2/transforms/mcpServerApi.test.ts b/packages/codemod/test/v1-to-v2/transforms/mcpServerApi.test.ts index b18a1abb3f..461cfb5da0 100644 --- a/packages/codemod/test/v1-to-v2/transforms/mcpServerApi.test.ts +++ b/packages/codemod/test/v1-to-v2/transforms/mcpServerApi.test.ts @@ -307,4 +307,63 @@ describe('mcp-server-api transform', () => { expect(result).toContain('registerTool("ping", {}'); expect(result).not.toContain('z.object'); }); + + it('flags taskStore in McpServer options as removed without modifying code', () => { + const input = [ + `const server = new McpServer(`, + ` { name: 'test', version: '1.0' },`, + ` { taskStore: new InMemoryTaskStore() }`, + `);`, + '' + ].join('\n'); + const project = new Project({ useInMemoryFileSystem: true }); + const sourceFile = project.createSourceFile('test.ts', MCP_IMPORT + input); + const result = mcpServerApiTransform.apply(sourceFile, ctx); + expect(sourceFile.getFullText()).toBe(MCP_IMPORT + input); + const taskDiags = result.diagnostics.filter(d => d.message.includes("'taskStore'")); + expect(taskDiags).toHaveLength(1); + expect(taskDiags[0]!.message).toContain('experimental tasks removed in v2 (SEP-2663'); + expect(taskDiags[0]!.message).toContain('No v2 equivalent'); + expect(taskDiags[0]!.insertComment).toBe(true); + }); + + it('flags each task option separately when both are present', () => { + const input = [ + `const server = new McpServer(`, + ` { name: 'test', version: '1.0' },`, + ` { taskStore: store, taskMessageQueue: queue }`, + `);`, + '' + ].join('\n'); + const project = new Project({ useInMemoryFileSystem: true }); + const sourceFile = project.createSourceFile('test.ts', MCP_IMPORT + input); + const result = mcpServerApiTransform.apply(sourceFile, ctx); + expect(sourceFile.getFullText()).toBe(MCP_IMPORT + input); + expect(result.diagnostics.some(d => d.message.includes("'taskStore'"))).toBe(true); + expect(result.diagnostics.some(d => d.message.includes("'taskMessageQueue'"))).toBe(true); + }); + + it('does not move task options into capabilities.tasks even when present', () => { + const input = [ + `const server = new McpServer(`, + ` { name: 'test', version: '1.0' },`, + ` { taskStore: store, capabilities: { tasks: {} } }`, + `);`, + '' + ].join('\n'); + const project = new Project({ useInMemoryFileSystem: true }); + const sourceFile = project.createSourceFile('test.ts', MCP_IMPORT + input); + const result = mcpServerApiTransform.apply(sourceFile, ctx); + expect(sourceFile.getFullText()).toBe(MCP_IMPORT + input); + expect(sourceFile.getFullText()).toContain('taskStore: store'); + expect(result.diagnostics.some(d => d.message.includes("'taskStore'"))).toBe(true); + }); + + it('emits no task diagnostics for McpServer options without task options', () => { + const input = [`const server = new McpServer({ name: 'test', version: '1.0' }, { instructions: 'hi' });`, ''].join('\n'); + const project = new Project({ useInMemoryFileSystem: true }); + const sourceFile = project.createSourceFile('test.ts', MCP_IMPORT + input); + const result = mcpServerApiTransform.apply(sourceFile, ctx); + expect(result.diagnostics).toHaveLength(0); + }); }); diff --git a/packages/core/src/errors/sdkErrors.ts b/packages/core/src/errors/sdkErrors.ts index af432c6389..1f77d1faca 100644 --- a/packages/core/src/errors/sdkErrors.ts +++ b/packages/core/src/errors/sdkErrors.ts @@ -28,6 +28,20 @@ export enum SdkErrorCode { SendFailed = 'SEND_FAILED', /** Response result failed local schema validation */ InvalidResult = 'INVALID_RESULT', + /** + * The response carried a `resultType` discriminator (protocol revision + * 2026-07-28) naming a result kind this client cannot consume yet, e.g. + * `input_required`. The kind is carried in `data.resultType`. + */ + UnsupportedResultType = 'UNSUPPORTED_RESULT_TYPE', + /** + * The spec method being sent does not exist on the negotiated protocol + * version's wire era (e.g. `tasks/get` toward a 2026-07-28 peer, or + * `server/discover` toward a 2025-era peer). Raised locally, before + * anything reaches the transport. The method and era are carried in + * `data.method` / `data.era`. + */ + MethodNotSupportedByProtocolVersion = 'METHOD_NOT_SUPPORTED_BY_PROTOCOL_VERSION', // Transport errors ClientHttpNotImplemented = 'CLIENT_HTTP_NOT_IMPLEMENTED', diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index a704267ee3..fc022586f5 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -10,9 +10,16 @@ export * from './shared/transport.js'; export * from './shared/uriTemplate.js'; export * from './types/index.js'; export * from './util/inMemory.js'; +// Wire-codec internals: ONLY the version→codec resolver the sibling packages +// need (era state itself lives on Protocol and is reached through the +// package-internal accessors exported by shared/protocol.ts). Nothing +// per-revision (schemas, registries, codec objects) is ever exported — not +// even on this internal barrel — so per-era vocabulary cannot leak toward the +// public surface. export * from './util/schema.js'; export * from './util/standardSchema.js'; export * from './util/zodCompat.js'; +export { codecForVersion } from './wire/codec.js'; // Validator providers are type-only here — import the runtime classes from the explicit // `@modelcontextprotocol/{core,client,server}/validators/{ajv,cf-worker}` subpaths to customise. diff --git a/packages/core/src/shared/protocol.ts b/packages/core/src/shared/protocol.ts index 16d2181018..f9b9555171 100644 --- a/packages/core/src/shared/protocol.ts +++ b/packages/core/src/shared/protocol.ts @@ -24,6 +24,7 @@ import type { Request, RequestId, RequestMeta, + RequestMetaEnvelope, RequestMethod, RequestTypeMap, Result, @@ -31,19 +32,23 @@ import type { ServerCapabilities } from '../types/index.js'; import { - getNotificationSchema, - getRequestSchema, - getResultSchema, + CLIENT_CAPABILITIES_META_KEY, + CLIENT_INFO_META_KEY, isJSONRPCErrorResponse, isJSONRPCNotification, isJSONRPCRequest, isJSONRPCResultResponse, + LOG_LEVEL_META_KEY, + PROTOCOL_VERSION_META_KEY, ProtocolError, ProtocolErrorCode, SUPPORTED_PROTOCOL_VERSIONS } from '../types/index.js'; import type { StandardSchemaV1 } from '../util/standardSchema.js'; import { isStandardSchema, validateStandardSchema } from '../util/standardSchema.js'; +import { bootstrapOutboundCodec } from '../wire/bootstrap.js'; +import type { LiftedWireMaterial, WireCodec } from '../wire/codec.js'; +import { classifiedWireEra, codecForVersion, isSpecNotificationMethod, isSpecRequestMethod } from '../wire/codec.js'; import type { Transport, TransportSendOptions } from './transport.js'; /** @@ -131,6 +136,79 @@ export type NotificationOptions = { relatedRequestId?: RequestId; }; +/** + * The reserved per-request `_meta` envelope keys (protocol revision + * 2026-07-28). The protocol layer lifts these out of inbound `_meta` before + * handlers run and surfaces them at `ctx.mcpReq.envelope` — they are + * wire-level bookkeeping, not handler material. + */ +const RESERVED_ENVELOPE_META_KEYS: readonly string[] = [ + PROTOCOL_VERSION_META_KEY, + CLIENT_INFO_META_KEY, + CLIENT_CAPABILITIES_META_KEY, + LOG_LEVEL_META_KEY +]; + +/** + * Top-level params members carrying multi-round-trip driver material + * (protocol revision 2026-07-28). The spec reserves these names on + * client-initiated REQUESTS only — notification params keep them untouched + * (a vendor notification may legitimately use the same names). + */ +const RETRY_PARAMS_KEYS = ['inputResponses', 'requestState'] as const; + +/** + * Lift wire-only material out of an inbound message so handlers see exactly + * the 2025-era shape, and surface it for the protocol layer (requests: via + * `ctx.mcpReq`). What counts as wire-only depends on the message kind: the + * reserved envelope `_meta` keys are reserved on every message, while the + * multi-round-trip retry fields (`inputResponses`/`requestState`) are + * reserved on client-initiated requests only — so notifications get only the + * envelope lift, and their top-level params stay untouched. Messages without + * wire-only material are returned unchanged (same reference). + */ +function liftWireOnlyMaterial( + message: T, + kind: 'request' | 'notification' +): { message: T; lifted: LiftedWireMaterial } { + const params = (message as { params?: unknown }).params; + if (!isPlainObject(params)) return { message, lifted: {} }; + + const meta = params._meta; + const envelopeKeys = isPlainObject(meta) ? RESERVED_ENVELOPE_META_KEYS.filter(key => key in meta) : []; + const retryKeys = kind === 'request' ? RETRY_PARAMS_KEYS.filter(key => key in params) : []; + if (envelopeKeys.length === 0 && retryKeys.length === 0) return { message, lifted: {} }; + + const lifted: LiftedWireMaterial = {}; + const nextParams: Record = { ...params }; + + if (envelopeKeys.length > 0 && isPlainObject(meta)) { + const envelope: Record = {}; + const nextMeta: Record = { ...meta }; + for (const key of envelopeKeys) { + envelope[key] = meta[key]; + delete nextMeta[key]; + } + // Surfaced as received; validation/enforcement is the dispatch-time + // classifier's job, not the lift's. + lifted.envelope = envelope as Partial; + if (Object.keys(nextMeta).length > 0) { + nextParams._meta = nextMeta; + } else { + delete nextParams._meta; + } + } + + for (const key of retryKeys) { + // Driver material reaches the protocol layer un-deleted, verbatim. + if (key === 'inputResponses') lifted.inputResponses = nextParams[key] as Record; + if (key === 'requestState') lifted.requestState = nextParams[key] as string; + delete nextParams[key]; + } + + return { message: { ...message, params: nextParams } as T, lifted }; +} + /** * Base context provided to all request handlers. */ @@ -155,10 +233,37 @@ export type BaseContext = { method: string; /** - * Metadata from the original request. + * Metadata from the original request, with the reserved + * `io.modelcontextprotocol/*` envelope keys already lifted out + * (readable via `ctx.mcpReq.envelope`). */ _meta?: RequestMeta; + /** + * The per-request `_meta` envelope (protocol revision 2026-07-28): + * the reserved `io.modelcontextprotocol/*` keys carried by the + * request, lifted out of the `_meta` the handler sees. Surfaced as + * received — `Partial` because only the keys the request actually + * carried are present (envelope requiredness is enforced per request + * at dispatch time, not by the lift); only present at all when the + * request carried envelope keys. + */ + envelope?: Partial; + + /** + * Multi-round-trip input responses carried by a retried request + * (protocol revision 2026-07-28), lifted out of the params the + * handler sees. Driver material — present verbatim when sent. + */ + inputResponses?: Record; + + /** + * Multi-round-trip request state echoed by a retried request + * (protocol revision 2026-07-28), lifted out of the params the + * handler sees. Driver material — present verbatim when sent. + */ + requestState?: string; + /** * An abort signal used to communicate if the request was cancelled from the sender's side. */ @@ -273,6 +378,45 @@ type TimeoutInfo = { onTimeout: () => void; }; +/* + * Package-internal access to Protocol's negotiated-protocol-version state. + * + * The negotiated version is a TS-private field on Protocol (it is connection + * state, not public surface — it never appears in the published declaration + * reports). The role classes (Client/Server), tests, and the modern-era + * server entry still need to read and write it at their lifecycle points, so + * Protocol's static initializer hands these module-scoped closures privileged + * access and the two functions below re-export them on the core INTERNAL + * barrel only. This is the F-2-style package-internal hook — deliberately not + * public API. + */ +let readNegotiatedProtocolVersion: (instance: Protocol) => string | undefined; +let writeNegotiatedProtocolVersion: (instance: Protocol, version: string | undefined) => void; + +/** + * Package-internal read channel for the protocol version a {@linkcode Protocol} + * instance has negotiated (`undefined` before negotiation). Exported on the + * core internal barrel only — never public API. + */ +export function negotiatedProtocolVersionOf(instance: Protocol): string | undefined { + return readNegotiatedProtocolVersion(instance); +} + +/** + * Package-internal write channel for a {@linkcode Protocol} instance's + * negotiated protocol version — the single era set/clear point outside the + * class itself. Called by `Client.connect` (fresh-connect clear + handshake + * completion), `Server._oninitialize`, tests, and the (future) modern-era + * server entry when it marks a factory instance modern at binding time. + * Exported on the core internal barrel only — never public API. + */ +export function setNegotiatedProtocolVersion( + instance: Protocol, + version: string | undefined +): void { + writeNegotiatedProtocolVersion(instance, version); +} + /** * Implements MCP protocol framing on top of a pluggable transport, including * features like request/response linking, notifications, and progress. @@ -285,12 +429,37 @@ export abstract class Protocol { private _requestMessageId = 0; private _requestHandlers: Map Promise> = new Map(); private _requestHandlerAbortControllers: Map = new Map(); - private _notificationHandlers: Map Promise> = new Map(); + private _notificationHandlers: Map Promise> = new Map(); private _responseHandlers: Map void> = new Map(); private _progressHandlers: Map = new Map(); private _timeoutInfo: Map = new Map(); private _pendingDebouncedNotifications = new Set(); + /** + * The protocol version negotiated for the current connection — the single + * source of truth for the wire era this instance speaks (Q1-SD1: the + * negotiated version cashes out as the negotiated wire ERA). + * + * Ordinary connection state, no side tables: + * - `Client.connect` clears it at the start of a fresh connect (the + * handshake itself runs pre-negotiation) and sets it once the handshake + * completes; the resume path keeps the original negotiation. + * - `Server._oninitialize` sets it when answering the legacy handshake; + * modern-era server instances get it set at instance binding through + * the package-internal hook ({@linkcode setNegotiatedProtocolVersion}). + * + * `undefined` = not negotiated yet: outbound lifecycle messages ride the + * bootstrap method pins and everything else defaults to the legacy era. + */ + private _negotiatedProtocolVersion?: string; + + static { + readNegotiatedProtocolVersion = instance => instance._negotiatedProtocolVersion; + writeNegotiatedProtocolVersion = (instance, version) => { + instance._negotiatedProtocolVersion = version; + }; + } + protected _supportedProtocolVersions: string[]; /** @@ -423,7 +592,7 @@ export abstract class Protocol { } else if (isJSONRPCRequest(message)) { this._onrequest(message, extra); } else if (isJSONRPCNotification(message)) { - this._onnotification(message); + this._onnotification(message, extra); } else { this._onerror(new Error(`Unknown message type: ${JSON.stringify(message)}`)); } @@ -470,44 +639,158 @@ export abstract class Protocol { this.onerror?.(error); } - private _onnotification(notification: JSONRPCNotification): void { - const handler = this._notificationHandlers.get(notification.method) ?? this.fallbackNotificationHandler; + private _onnotification(rawNotification: JSONRPCNotification, extra?: MessageExtraInfo): void { + // Hide wire-only material from notification handlers too — but ONLY + // the reserved envelope `_meta` keys (the retry params names are + // reserved on requests, not notifications). There is no + // per-notification context, so the lifted envelope keys are dropped, + // not surfaced; the protocol layer owns them. + const { message: notification } = liftWireOnlyMaterial(rawNotification, 'notification'); + + // Era is instance state: the negotiated protocol version selects the + // codec for everything this connection receives (legacy until + // negotiated). Classification is no longer a per-message era switch — + // it is validated against the instance era below. + const codec = this._negotiatedWireCodec(); + + // Edge→instance handoff check: a classification that disagrees with + // the instance era means the entry routed another era's traffic onto + // this instance. That is a routing error — drop the notification and + // surface it out of band; never serve it on a guessed era. + if (extra?.classification !== undefined) { + const classified = classifiedWireEra(extra.classification); + if (classified !== codec.era) { + this._onerror( + new Error( + `Era mismatch on inbound notification '${notification.method}': classified as ${classified} but this instance serves ${codec.era}` + ) + ); + return; + } + } + + // Era gate — deletions are physical: a spec notification that is not + // in this era's registry is dropped even when a handler is + // registered (notifications get no error response; silent drop is + // the protocol-correct outcome, matching today's unknown-method + // posture). Methods outside the spec universe are consumer-owned + // extension notifications and stay era-blind. + if (isSpecNotificationMethod(notification.method) && !codec.hasNotificationMethod(notification.method)) { + return; + } + + const handler = this._notificationHandlers.get(notification.method); + const fallback = this.fallbackNotificationHandler; // Ignore notifications not being subscribed to. - if (handler === undefined) { + if (handler === undefined && fallback === undefined) { return; } // Starting with Promise.resolve() puts any synchronous errors into the monad as well. Promise.resolve() - .then(() => handler(notification)) + .then(() => (handler === undefined ? fallback!(notification) : handler(notification, codec))) .catch(error => this._onerror(new Error(`Uncaught error in notification handler: ${error}`))); } - private _onrequest(request: JSONRPCRequest, extra?: MessageExtraInfo): void { - const handler = this._requestHandlers.get(request.method) ?? this.fallbackRequestHandler; + private _onrequest(rawRequest: JSONRPCRequest, extra?: MessageExtraInfo): void { + // Lift wire-only material before dispatch: handlers (including the + // fallback handler and the per-method schema parse) see exactly the + // 2025-era shape; the envelope and retry fields surface via ctx. + const { message: request, lifted } = liftWireOnlyMaterial(rawRequest, 'request'); + + // Era is instance state: the negotiated protocol version selects the + // codec for everything this connection receives (legacy until + // negotiated). Classification (Q2; this layer only CONSUMES + // MessageExtraInfo.classification) is no longer a per-message era + // switch — it is validated against the instance era below. Hand-wired + // legacy transports never classify, so their behavior is untouched. + const codec = this._negotiatedWireCodec(); // Capture the current transport at request time to ensure responses go to the correct client const capturedTransport = this._transport; - const sendNotification = (notification: Notification, options?: NotificationOptions) => - this.notification(notification, { ...options, relatedRequestId: request.id }); - const sendRequest = (r: Request, resultSchema: U, options?: RequestOptions) => - this._requestWithSchema(r, resultSchema, { ...options, relatedRequestId: request.id }); - - if (handler === undefined) { + const sendErrorResponse = (code: number, message: string, data?: unknown) => { const errorResponse: JSONRPCErrorResponse = { jsonrpc: '2.0', id: request.id, - error: { - code: ProtocolErrorCode.MethodNotFound, - message: 'Method not found' - } + error: { code, message, ...(data !== undefined && { data }) } }; capturedTransport?.send(errorResponse).catch(error => this._onerror(new Error(`Failed to send an error response: ${error}`))); + }; + + // Edge→instance handoff check: a classification that disagrees with + // the instance era means the entry routed another era's traffic onto + // this instance. That is a routing error: answer with the typed era + // error (−32004 Unsupported protocol version) and surface it out of + // band — never serve the request on a guessed era. + if (extra?.classification !== undefined) { + const classified = classifiedWireEra(extra.classification); + if (classified !== codec.era) { + this._onerror( + new Error( + `Era mismatch on inbound request '${request.method}': classified as ${classified} but this instance serves ${codec.era}` + ) + ); + sendErrorResponse(ProtocolErrorCode.UnsupportedProtocolVersion, `Unsupported protocol version: ${classified}`, { + // Per spec, `supported` is the full list of protocol + // versions the receiver supports — not just the version + // this connection is on — so the peer can pick a mutually + // supported version from the error alone. + supported: this._supportedProtocolVersions, + requested: classified + }); + return; + } + } + + // Era gate — deletions are physical: a spec method that is not in + // this era's registry is −32601 BY ABSENCE, before any handler + // lookup, even when a handler is registered (a custom handler cannot + // shadow a deleted spec method across eras). Methods outside the + // spec universe are consumer-owned extension methods and stay + // era-blind. + if (isSpecRequestMethod(request.method) && !codec.hasRequestMethod(request.method)) { + sendErrorResponse(ProtocolErrorCode.MethodNotFound, 'Method not found'); + return; + } + + const handler = this._requestHandlers.get(request.method) ?? this.fallbackRequestHandler; + + if (handler === undefined) { + sendErrorResponse(ProtocolErrorCode.MethodNotFound, 'Method not found'); + return; + } + + // Envelope enforcement: the 2026 era requires the per-request `_meta` + // envelope on every request (spec.types.2026-07-28 RequestParams). + // The lift extracted it above; the era codec validates requiredness. + // Deliberately AFTER the era gate and the handler-existence check: + // an unknown method answers −32601 even when the envelope is also + // missing — method existence outranks parameter validity. (The + // canonical precedence table for the full inbound validation ladder + // arrives with the validation-ladder milestone; this site encodes + // only the −32601-over-−32602 rule.) + const envelopeError = codec.checkInboundEnvelope(lifted); + if (envelopeError !== undefined) { + sendErrorResponse(ProtocolErrorCode.InvalidParams, envelopeError); return; } + // Related sends resolve through the SAME instance era as every other + // sender (the per-request/instance asymmetry is deliberately gone): + // the codec is resolved at send time from the connection state. + const sendNotification = (notification: Notification, options?: NotificationOptions) => + this._notificationViaCodec(this._resolveOutboundCodec(notification.method), notification, { + ...options, + relatedRequestId: request.id + }); + const sendRequest = (r: Request, resultSchema: U, options?: RequestOptions) => + this._requestWithSchemaViaCodec(this._resolveOutboundCodec(r.method), r, resultSchema, { + ...options, + relatedRequestId: request.id + }); + const abortController = new AbortController(); this._requestHandlerAbortControllers.set(request.id, abortController); @@ -517,16 +800,24 @@ export abstract class Protocol { id: request.id, method: request.method, _meta: request.params?._meta, + ...(lifted.envelope !== undefined && { envelope: lifted.envelope }), + ...(lifted.inputResponses !== undefined && { inputResponses: lifted.inputResponses }), + ...(lifted.requestState !== undefined && { requestState: lifted.requestState }), signal: abortController.signal, // BaseContext.mcpReq.send is declared with two overloads (spec-method-keyed and explicit-schema). Arrow // literals can't carry overload signatures, so the inferred single-signature type isn't assignable to // that overloaded property type. The cast is sound: this impl dispatches both overload paths via the // isStandardSchema guard, and sendRequest validates the result against the resolved schema either way. send: ((r: Request, schemaOrOptions?: StandardSchemaV1 | RequestOptions, maybeOptions?: RequestOptions) => { + // Related requests resolve through the instance era at + // send time, exactly like direct sends: era-gate first, + // then method-keyed schema resolution. + const sendCodec = this._resolveOutboundCodec(r.method); + this._assertOutboundRequestInEra(sendCodec, r.method); if (isStandardSchema(schemaOrOptions)) { return sendRequest(r, schemaOrOptions, maybeOptions); } - const resultSchema = getResultSchema(r.method); + const resultSchema = sendCodec.resultSchema(r.method); if (!resultSchema) { throw new TypeError( `'${r.method}' is not a spec method; pass a result schema as the second argument to ctx.mcpReq.send().` @@ -550,8 +841,25 @@ export abstract class Protocol { return; } + // The outbound stamp seam: the era codec maps the neutral + // handler result to its wire shape. The 2025-era codec is + // the identity (never-stamp); the 2026-era codec stamps + // `resultType` and enforces the deleted-field set. A throw + // here is a NEW failure mode between handler success and + // the transport send (and the seam grows ttlMs/cacheScope + // stamping content in M3.2) — it must answer the peer with + // −32603 rather than stranding the request until timeout. + let encoded: Result; + try { + encoded = codec.encodeResult(request.method, result); + } catch (error) { + this._onerror(new Error(`Failed to encode result for ${request.method}: ${error}`)); + sendErrorResponse(ProtocolErrorCode.InternalError, 'Internal error'); + return; + } + const response: JSONRPCResponse = { - result, + result: encoded, jsonrpc: '2.0', id: request.id }; @@ -685,26 +993,91 @@ export abstract class Protocol { options?: RequestOptions ): Promise>; request(request: Request, schemaOrOptions?: StandardSchemaV1 | RequestOptions, maybeOptions?: RequestOptions): Promise { + const codec = this._resolveOutboundCodec(request.method); + this._assertOutboundRequestInEra(codec, request.method); if (isStandardSchema(schemaOrOptions)) { - return this._requestWithSchema(request, schemaOrOptions, maybeOptions); + return this._requestWithSchemaViaCodec(codec, request, schemaOrOptions, maybeOptions); } - const resultSchema = getResultSchema(request.method); + const resultSchema = codec.resultSchema(request.method); if (!resultSchema) { throw new TypeError(`'${request.method}' is not a spec method; pass a result schema as the second argument to request().`); } - return this._requestWithSchema(request, resultSchema, schemaOrOptions); + return this._requestWithSchemaViaCodec(codec, request, resultSchema, schemaOrOptions); + } + + /** + * The wire codec for this instance's negotiated era — the phase-2 truth: + * everything an established connection sends and receives resolves + * through it. Legacy until a version has been negotiated. + */ + private _negotiatedWireCodec(): WireCodec { + return codecForVersion(this._negotiatedProtocolVersion); + } + + /** + * Outbound codec resolution: while the negotiated version is still unset + * (the negotiation window), lifecycle messages are bootstrap-pinned BY + * METHOD — they self-identify their era (`initialize` IS the legacy + * handshake, `server/discover` IS the modern probe). Once a version has + * been negotiated, the instance era is authoritative for everything — a + * negotiated session never re-routes a method onto the other era. + */ + private _resolveOutboundCodec(method: string): WireCodec { + if (this._negotiatedProtocolVersion === undefined) { + const pinned = bootstrapOutboundCodec(method); + if (pinned) return pinned; + } + return this._negotiatedWireCodec(); + } + + /** + * Era gate for outbound requests — deletions are physical in BOTH + * directions: sending a spec method that the resolved era does not define + * dies locally with a typed error before anything reaches the transport. + * Methods outside the spec universe are consumer-owned extension methods + * and stay era-blind. + */ + private _assertOutboundRequestInEra(codec: WireCodec, method: string): void { + if (isSpecRequestMethod(method) && !codec.hasRequestMethod(method)) { + throw new SdkError( + SdkErrorCode.MethodNotSupportedByProtocolVersion, + `Method '${method}' is not supported by the negotiated protocol version (wire era ${codec.era})`, + { method, era: codec.era } + ); + } } /** - * Sends a request and waits for a response, using the provided schema for validation. + * Sends a request and waits for a response, using the provided schema for + * validation instead of the era registry's method-keyed entry. * - * This is the internal implementation used by SDK methods that need to specify - * a particular result schema (e.g., for compatibility schemas). + * This is the internal implementation used by SDK methods whose result + * schema cannot be expressed as a method-keyed registry entry — the one + * surviving case is `server.createMessage`, whose result schema depends + * on the REQUEST params (tools vs no tools) — and by callers passing + * explicit compatibility schemas. Spec methods are still era-gated here: + * an explicit schema never smuggles a deleted method onto the wire. */ protected _requestWithSchema( request: Request, resultSchema: T, options?: RequestOptions + ): Promise> { + const codec = this._resolveOutboundCodec(request.method); + this._assertOutboundRequestInEra(codec, request.method); + return this._requestWithSchemaViaCodec(codec, request, resultSchema, options); + } + + /** + * The request funnel proper, keyed by the resolved era codec: the codec + * owns result decoding (raw-first `resultType` discrimination — V-1 — + * and the era's lift posture) before the schema validation step. + */ + private _requestWithSchemaViaCodec( + codec: WireCodec, + request: Request, + resultSchema: T, + options?: RequestOptions ): Promise> { const { relatedRequestId, resumptionToken, onresumptiontoken } = options ?? {}; @@ -789,7 +1162,43 @@ export abstract class Protocol { return reject(response); } - validateStandardSchema(resultSchema, response.result).then(parseResult => { + // Codec decode hop — the structural V-1 home. The era codec + // owns the raw-first resultType postures (Q1-SD3): + // - 2026 era: REQUIRED discriminator; absent → typed error + // naming the spec violation; input_required → driver seam; + // unknown kind → invalid, no retry; complete → wire-exact + // parse then lift. + // - 2025 era: resultType is foreign vocabulary → strip-on- + // lift, then today's schema validation decides. + // Either way a non-complete body can never be masked into a + // hollow success by a tolerant result schema. + // Guarded: this callback runs synchronously inside + // `_onresponse`, so a throw out of the decode hop would + // otherwise propagate into the transport's onmessage instead + // of failing this request. + let decoded: ReturnType; + try { + decoded = codec.decodeResult(request.method, response.result); + } catch (error) { + return reject(error instanceof Error ? error : new Error(String(error))); + } + if (decoded.kind === 'invalid') { + return reject(decoded.error); + } + if (decoded.kind === 'input_required') { + // Driver seam: the multi-round-trip driver (M4.1) + // consumes this payload; until it lands, surface the + // discriminated kind as a typed local error, no retry. + return reject( + new SdkError(SdkErrorCode.UnsupportedResultType, `Unsupported result type 'input_required' for ${request.method}`, { + resultType: 'input_required', + method: request.method + }) + ); + } + const result = decoded.result; + + validateStandardSchema(resultSchema, result).then(parseResult => { if (parseResult.success) { resolve(parseResult.data); } else { @@ -829,10 +1238,29 @@ export abstract class Protocol { * Emits a notification, which is a one-way message that does not expect a response. */ async notification(notification: Notification, options?: NotificationOptions): Promise { + return this._notificationViaCodec(this._resolveOutboundCodec(notification.method), notification, options); + } + + /** + * The notification funnel proper, keyed by the resolved era codec — + * direct sends and related notifications (`ctx.mcpReq.notify`) alike + * resolve through the instance's negotiated era at send time. + */ + private async _notificationViaCodec(codec: WireCodec, notification: Notification, options?: NotificationOptions): Promise { if (!this._transport) { throw new SdkError(SdkErrorCode.NotConnected, 'Not connected'); } + // Era gate — outbound deletions are physical for notifications too: a + // spec notification the resolved era does not define dies locally. + if (isSpecNotificationMethod(notification.method) && !codec.hasNotificationMethod(notification.method)) { + throw new SdkError( + SdkErrorCode.MethodNotSupportedByProtocolVersion, + `Notification '${notification.method}' is not supported by the negotiated protocol version (wire era ${codec.era})`, + { method: notification.method, era: codec.era } + ); + } + this.assertNotificationCapability(notification.method); const jsonrpcNotification: JSONRPCNotification = { jsonrpc: '2.0', ...notification }; @@ -914,18 +1342,32 @@ export abstract class Protocol { let stored: (request: JSONRPCRequest, ctx: ContextT) => Promise; if (typeof schemasOrHandler === 'function') { - const schema = getRequestSchema(method); - if (!schema) { + if (!isSpecRequestMethod(method)) { throw new TypeError( `'${method}' is not a spec request method; pass schemas as the second argument to setRequestHandler().` ); } - stored = (request, ctx) => Promise.resolve(schemasOrHandler(schema.parse(request), ctx)); + // Dispatch-time schema resolution: the request is parsed with the + // schema of the era serving this connection (the instance era at + // dispatch time), never with a schema captured at registration + // time. + stored = (request, ctx) => { + const schema = this._negotiatedWireCodec().requestSchema(method); + if (!schema) { + // Unreachable: the dispatch era gate rejects era-mismatched + // spec methods with −32601 before any handler runs. + throw new ProtocolError(ProtocolErrorCode.InternalError, `No wire schema for ${method} in the resolved era`); + } + return Promise.resolve(schemasOrHandler(schema.parse(request), ctx)); + }; } else if (maybeHandler) { stored = async (request, ctx) => { - const userParams = { ...request.params }; - delete userParams._meta; - const parsed = await validateStandardSchema(schemasOrHandler.params, userParams); + // Custom handlers receive `_meta` present-minus-reserved: the + // wire-only lift already removed the reserved envelope keys, + // and the remaining metadata (progressToken, extension keys) + // is handler material — consistent with the spec-method path. + // (Behavior migration: `_meta` used to be deleted here.) + const parsed = await validateStandardSchema(schemasOrHandler.params, { ...request.params }); if (!parsed.success) { throw new ProtocolError(ProtocolErrorCode.InvalidParams, `Invalid params for ${method}: ${parsed.error}`); } @@ -978,7 +1420,9 @@ export abstract class Protocol { * spec schema. For custom (non-spec) methods, pass `(method, schemas, handler)`; * `params` are validated against `schemas.params` and the handler receives the * parsed params object directly. The raw notification is passed as the second - * argument; `_meta` is recoverable via `notification.params?._meta`. + * argument; `_meta` is recoverable via `notification.params?._meta` (minus the + * reserved `io.modelcontextprotocol/*` envelope keys, which the protocol layer + * lifts out before dispatch). */ setNotificationHandler( method: M, @@ -995,13 +1439,22 @@ export abstract class Protocol { maybeHandler?: (params: unknown, notification: Notification) => void | Promise ): void { if (typeof schemasOrHandler === 'function') { - const schema = getNotificationSchema(method); - if (!schema) { + if (!isSpecNotificationMethod(method)) { throw new TypeError( `'${method}' is not a spec notification method; pass schemas as the second argument to setNotificationHandler().` ); } - this._notificationHandlers.set(method, notification => Promise.resolve(schemasOrHandler(schema.parse(notification)))); + // Dispatch-time schema resolution, same as setRequestHandler: the + // era serving the message picks the schema. + this._notificationHandlers.set(method, (notification, codec) => { + const schema = codec.notificationSchema(method); + if (!schema) { + // Unreachable: the dispatch era gate drops era-mismatched + // spec notifications before any handler runs. + throw new ProtocolError(ProtocolErrorCode.InternalError, `No wire schema for ${method} in the resolved era`); + } + return Promise.resolve(schemasOrHandler(schema.parse(notification))); + }); return; } @@ -1009,9 +1462,9 @@ export abstract class Protocol { throw new TypeError('setNotificationHandler: handler is required'); } this._notificationHandlers.set(method, async notification => { - const userParams = { ...notification.params }; - delete userParams._meta; - const parsed = await validateStandardSchema(schemasOrHandler.params, userParams); + // `_meta` present-minus-reserved, matching the custom request + // path (the lift already removed the reserved envelope keys). + const parsed = await validateStandardSchema(schemasOrHandler.params, { ...notification.params }); if (!parsed.success) { throw new ProtocolError(ProtocolErrorCode.InvalidParams, `Invalid params for notification ${method}: ${parsed.error}`); } diff --git a/packages/core/src/types/README.md b/packages/core/src/types/README.md new file mode 100644 index 0000000000..6d235ec8ae --- /dev/null +++ b/packages/core/src/types/README.md @@ -0,0 +1,26 @@ +# Spec reference types ("anchors") + +The `spec.types..ts` files in this directory are vendored, verbatim copies of the MCP specification's normative `schema.ts`, one file per protocol revision. Each file is generated by `pnpm run fetch:spec-types [version] [sha]` (`scripts/fetch-spec-types.ts`): the +upstream schema is fetched at a specific spec commit, a provenance header recording that commit is prepended, and the result is formatted with the project's prettier config — no other transformation. + +They are reference-only test oracles: the comparison suites in `packages/core/test/spec.types..test.ts` check the SDK's own types against them. They are not exported from any barrel and must never be imported by runtime code. + +## Lifecycle policy + +1. **Released revisions are frozen.** Once a protocol revision is published under `schema//` in the spec repository, its anchor regenerates only from the pinned spec commit recorded in `RELEASED_REVISION_PINS` (`scripts/fetch-spec-types.ts`) — never from the latest + upstream commit. Moving that pin, including the freeze of a newly published revision (when its generation source switches from `schema/draft/` to `schema//`), must land in the same commit that retargets the nightly update workflow + (`.github/workflows/update-spec-types.yml`), so the anchor and the automation that maintains it can never disagree about the source of truth. + +2. **Draft anchors float only via reviewed refresh PRs.** The anchor for an unreleased revision tracks the spec repository's `schema/draft/schema.ts`. The nightly workflow regenerates it from the latest upstream commit and, when the result differs from what is checked in, opens + (or updates) a refresh PR. Manual refreshes follow the same path: regenerate, then propose the diff in a PR. + +3. **The bot proposes; it never auto-merges.** Automated refreshes always go through a pull request that a maintainer reviews and merges. No automation pushes anchor changes directly to `main` or merges its own PRs. A refresh PR that breaks the comparison suites is the desired + signal — it is fixed in that PR, not bypassed. + +4. **Generated twins update atomically with their anchor.** If artifacts derived from an anchor (for example vendored JSON schemas or generated validators) are checked into this repository, any refresh that changes the anchor must regenerate those artifacts in the same commit. + The anchor and its derived twins must never be out of sync at any commit on `main`. + + **This clause is OPERATIVE.** The vendored twins are the per-revision `schema.json` copies under `packages/core/test/corpus/schema-twins/` (`.schema.json` + `manifest.json` recording the source commit and content hashes). They are TEST-ONLY oracles consumed by the + schema-twin conformance lock (`test/wire/schemaTwinConformance.test.ts`) — never bundled, never imported by runtime code, and the JSON Schema engines stay optional peer dependencies. A refresh of `spec.types..ts` must copy the matching upstream + `schema//schema.json` (same spec commit) over the twin and update `manifest.json` in the same commit; the spec example corpus manifest (`test/corpus/fixtures//manifest.json`) records its own source commit and follows the same atomicity rule when the examples + are re-vendored. The conformance lock failing after an anchor-only refresh is the desired loud signal of a missed twin update. diff --git a/packages/core/src/types/constants.ts b/packages/core/src/types/constants.ts index 018f9ecb51..109c5c4ee2 100644 --- a/packages/core/src/types/constants.ts +++ b/packages/core/src/types/constants.ts @@ -2,6 +2,11 @@ export const LATEST_PROTOCOL_VERSION = '2025-11-25'; export const DEFAULT_NEGOTIATED_PROTOCOL_VERSION = '2025-03-26'; export const SUPPORTED_PROTOCOL_VERSIONS = [LATEST_PROTOCOL_VERSION, '2025-06-18', '2025-03-26', '2024-11-05', '2024-10-07']; +/** + * `_meta` key associating a message with a 2025-11-25 task. + * + * @deprecated 2025-11-25 wire vocabulary with no SDK runtime; kept importable for interoperability only. + */ export const RELATED_TASK_META_KEY = 'io.modelcontextprotocol/related-task'; /* Reserved `_meta` keys for the per-request envelope (protocol revision 2026-07-28) */ diff --git a/packages/core/src/types/guards.ts b/packages/core/src/types/guards.ts index f385b91b42..8091b962c1 100644 --- a/packages/core/src/types/guards.ts +++ b/packages/core/src/types/guards.ts @@ -72,6 +72,12 @@ export const isJSONRPCResponse = (value: unknown): value is JSONRPCResponse => J /** * Checks if a value is a valid {@linkcode CallToolResult}. + * + * This is a consumer-side VALUE check against the neutral model, not a wire + * validator: a raw wire object that additionally carries wire-only members + * (e.g. `resultType`) still passes through the loose index signature. Use a + * transport-level parse to validate raw wire traffic. + * * @param value - The value to check. * * @returns True if the value is a valid {@linkcode CallToolResult}, false otherwise. @@ -86,6 +92,9 @@ export const isCallToolResult = (value: unknown): value is CallToolResult => { * @param value - The value to check. * * @returns True if the value is a valid {@linkcode TaskAugmentedRequestParams}, false otherwise. + * + * @deprecated Recognizes 2025-11-25 task wire vocabulary, which has no SDK + * runtime; kept importable for interoperability only. */ export const isTaskAugmentedRequestParams = (value: unknown): value is TaskAugmentedRequestParams => TaskAugmentedRequestParamsSchema.safeParse(value).success; diff --git a/packages/core/src/types/schemas.ts b/packages/core/src/types/schemas.ts index fe850284e2..eec110b960 100644 --- a/packages/core/src/types/schemas.ts +++ b/packages/core/src/types/schemas.ts @@ -1,23 +1,7 @@ import * as z from 'zod/v4'; -import { - CLIENT_CAPABILITIES_META_KEY, - CLIENT_INFO_META_KEY, - JSONRPC_VERSION, - LOG_LEVEL_META_KEY, - PROTOCOL_VERSION_META_KEY, - RELATED_TASK_META_KEY -} from './constants.js'; -import type { - JSONArray, - JSONObject, - JSONValue, - NotificationMethod, - NotificationTypeMap, - RequestMethod, - RequestTypeMap, - ResultTypeMap -} from './types.js'; +import { JSONRPC_VERSION, RELATED_TASK_META_KEY } from './constants.js'; +import type { JSONArray, JSONObject, JSONValue } from './types.js'; export const JSONValueSchema: z.ZodType = z.lazy(() => z.union([z.string(), z.number(), z.boolean(), z.null(), z.record(z.string(), JSONValueSchema), z.array(JSONValueSchema)]) @@ -34,21 +18,7 @@ export const ProgressTokenSchema = z.union([z.string(), z.number().int()]); */ export const CursorSchema = z.string(); -/** - * Task creation parameters, used to ask that the server create a task to represent a request. - */ -export const TaskCreationParamsSchema = z.looseObject({ - /** - * Requested duration in milliseconds to retain task from creation. - */ - ttl: z.number().optional(), - - /** - * Time in milliseconds to wait between task status requests. - */ - pollInterval: z.number().optional() -}); - +/** @deprecated 2025-11-25 wire vocabulary with no SDK runtime; kept importable for interoperability only. */ export const TaskMetadataSchema = z.object({ ttl: z.number().optional() }); @@ -56,6 +26,8 @@ export const TaskMetadataSchema = z.object({ /** * Metadata for associating messages with a task. * Include this in the `_meta` field under the key `io.modelcontextprotocol/related-task`. + * + * @deprecated 2025-11-25 wire vocabulary with no SDK runtime; kept importable for interoperability only. */ export const RelatedTaskMetadataSchema = z.object({ taskId: z.string() @@ -84,6 +56,8 @@ export const BaseRequestParamsSchema = z.object({ /** * Common params for any task-augmented request. + * + * @deprecated 2025-11-25 wire vocabulary with no SDK runtime; kept importable for interoperability only. */ export const TaskAugmentedRequestParamsSchema = BaseRequestParamsSchema.extend({ /** @@ -120,14 +94,13 @@ export const ResultSchema = z.looseObject({ * See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields) * for notes on `_meta` usage. */ - _meta: RequestMetaSchema.optional(), - /** - * Indicates the type of the result, allowing the receiver to determine how to - * parse the result object. Servers implementing protocol revision 2026-07-28 or - * later always include this field; results from earlier revisions omit it, and - * an absent value must be treated as `"complete"`. - */ - resultType: z.string().optional() + _meta: RequestMetaSchema.optional() + // `resultType` is wire-only vocabulary (protocol revision 2026-07-28) and + // is deliberately NOT modeled here: the neutral result schemas carry no + // slot for it. It exists only inside the 2026-era wire codec, which + // consumes it on decode and stamps it on encode. (Q1 increment 2 - the + // former optional member here was the masking surface that let modern + // vocabulary leak through every legacy-leg parse.) }); /** @@ -347,6 +320,8 @@ const ElicitationCapabilitySchema = z.preprocess( /** * Task capabilities for clients, indicating which request types support task creation. + * + * @deprecated 2025-11-25 wire vocabulary with no SDK runtime; kept importable for interoperability only. */ export const ClientTasksCapabilitySchema = z.looseObject({ /** @@ -384,6 +359,8 @@ export const ClientTasksCapabilitySchema = z.looseObject({ /** * Task capabilities for servers, indicating which request types support task creation. + * + * @deprecated 2025-11-25 wire vocabulary with no SDK runtime; kept importable for interoperability only. */ export const ServerTasksCapabilitySchema = z.looseObject({ /** @@ -460,6 +437,8 @@ export const ClientCapabilitiesSchema = z.object({ .optional(), /** * Present if the client supports task creation. + * + * @deprecated 2025-11-25 wire vocabulary with no SDK runtime; parsed for interoperability only — servers built on this SDK never advertise it. */ tasks: ClientTasksCapabilitySchema.optional(), /** @@ -544,6 +523,8 @@ export const ServerCapabilitiesSchema = z.object({ .optional(), /** * Present if the server supports task creation. + * + * @deprecated 2025-11-25 wire vocabulary with no SDK runtime; parsed for interoperability only — servers built on this SDK never advertise it. */ tasks: ServerTasksCapabilitySchema.optional(), /** @@ -679,120 +660,6 @@ export const PaginatedResultSchema = ResultSchema.extend({ nextCursor: CursorSchema.optional() }); -/** - * The status of a task. - * */ -export const TaskStatusSchema = z.enum(['working', 'input_required', 'completed', 'failed', 'cancelled']); - -/* Tasks */ -/** - * A pollable state object associated with a request. - */ -export const TaskSchema = z.object({ - taskId: z.string(), - status: TaskStatusSchema, - /** - * Time in milliseconds to keep task results available after completion. - * If `null`, the task has unlimited lifetime until manually cleaned up. - */ - ttl: z.union([z.number(), z.null()]), - /** - * ISO 8601 timestamp when the task was created. - */ - createdAt: z.string(), - /** - * ISO 8601 timestamp when the task was last updated. - */ - lastUpdatedAt: z.string(), - pollInterval: z.optional(z.number()), - /** - * Optional diagnostic message for failed tasks or other status information. - */ - statusMessage: z.optional(z.string()) -}); - -/** - * Result returned when a task is created, containing the task data wrapped in a `task` field. - */ -export const CreateTaskResultSchema = ResultSchema.extend({ - task: TaskSchema -}); - -/** - * Parameters for task status notification. - */ -export const TaskStatusNotificationParamsSchema = NotificationsParamsSchema.merge(TaskSchema); - -/** - * A notification sent when a task's status changes. - */ -export const TaskStatusNotificationSchema = NotificationSchema.extend({ - method: z.literal('notifications/tasks/status'), - params: TaskStatusNotificationParamsSchema -}); - -/** - * A request to get the state of a specific task. - */ -export const GetTaskRequestSchema = RequestSchema.extend({ - method: z.literal('tasks/get'), - params: BaseRequestParamsSchema.extend({ - taskId: z.string() - }) -}); - -/** - * The response to a {@linkcode GetTaskRequest | tasks/get} request. - */ -export const GetTaskResultSchema = ResultSchema.merge(TaskSchema); - -/** - * A request to get the result of a specific task. - */ -export const GetTaskPayloadRequestSchema = RequestSchema.extend({ - method: z.literal('tasks/result'), - params: BaseRequestParamsSchema.extend({ - taskId: z.string() - }) -}); - -/** - * The response to a `tasks/result` request. - * The structure matches the result type of the original request. - * For example, a {@linkcode CallToolRequest | tools/call} task would return the `CallToolResult` structure. - * - */ -export const GetTaskPayloadResultSchema = ResultSchema.loose(); - -/** - * A request to list tasks. - */ -export const ListTasksRequestSchema = PaginatedRequestSchema.extend({ - method: z.literal('tasks/list') -}); - -/** - * The response to a {@linkcode ListTasksRequest | tasks/list} request. - */ -export const ListTasksResultSchema = PaginatedResultSchema.extend({ - tasks: z.array(TaskSchema) -}); - -/** - * A request to cancel a specific task. - */ -export const CancelTaskRequestSchema = RequestSchema.extend({ - method: z.literal('tasks/cancel'), - params: BaseRequestParamsSchema.extend({ - taskId: z.string() - }) -}); - -/** - * The response to a {@linkcode CancelTaskRequest | tasks/cancel} request. - */ -export const CancelTaskResultSchema = ResultSchema.merge(TaskSchema); - /* Resources */ /** * The contents of a specific resource or sub-resource. @@ -1432,9 +1299,9 @@ export const CallToolResultSchema = ResultSchema.extend({ * A list of content objects that represent the result of the tool call. * * If the `Tool` does not define an outputSchema, this field MUST be present in the result. - * For backwards compatibility, this field is always present, but it may be empty. + * Required on the wire per the specification (it may be an empty array). */ - content: z.array(ContentBlockSchema).default([]), + content: z.array(ContentBlockSchema), /** * An object containing structured tool output. @@ -1572,48 +1439,6 @@ export const LoggingMessageNotificationSchema = NotificationSchema.extend({ params: LoggingMessageNotificationParamsSchema }); -/* Per-request `_meta` envelope */ -/** - * The per-request `_meta` envelope carried by every request under protocol revision - * 2026-07-28: the protocol version governing the request, the client implementation - * info, and the client's capabilities — declared per request rather than once at - * initialization — plus the optional log-level opt-in. - * - * This schema models the complete envelope on its own. The base request schemas - * ({@linkcode RequestMetaSchema}) deliberately stay lenient so the same wire schemas - * parse requests from earlier protocol revisions (no envelope) as well; envelope - * requiredness is enforced per request at dispatch time, not here. - */ -export const RequestMetaEnvelopeSchema = z.looseObject({ - /** - * If specified, the caller is requesting out-of-band progress notifications for this request (as represented by 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: ProgressTokenSchema.optional(), - /** - * The MCP protocol version being used for this request. For the HTTP transport, - * the value must match the `MCP-Protocol-Version` header. - */ - [PROTOCOL_VERSION_META_KEY]: z.string(), - /** - * Identifies the client software making the request. - */ - [CLIENT_INFO_META_KEY]: ImplementationSchema, - /** - * The client's capabilities for this specific request. An empty object means the - * client supports no optional capabilities. Servers must not infer capabilities - * from prior requests. - */ - [CLIENT_CAPABILITIES_META_KEY]: ClientCapabilitiesSchema, - /** - * The desired log level for this request. When absent, the server must not send - * `notifications/message` notifications for the request. - * - * @deprecated Deprecated as of protocol version 2026-07-28 (SEP-2577); remains - * in the specification for at least twelve months. - */ - [LOG_LEVEL_META_KEY]: LoggingLevelSchema.optional() -}); - /* Sampling */ /** * Hints to use for model selection. @@ -1667,7 +1492,7 @@ export const ToolChoiceSchema = z.object({ export const ToolResultContentSchema = z.object({ type: z.literal('tool_result'), toolUseId: z.string().describe('The unique identifier for the corresponding tool call.'), - content: z.array(ContentBlockSchema).default([]), + content: z.array(ContentBlockSchema), structuredContent: z.object({}).loose().optional(), isError: z.boolean().optional(), @@ -2181,6 +2006,12 @@ export const RootsListChangedNotificationSchema = NotificationSchema.extend({ }); /* Client messages */ +// NOTE (Q1 increment 2): the role unions below are the NEUTRAL message sets. +// The 2025-era task vocabulary (tasks/* methods, task results, the task +// status notification) is 2025-only WIRE vocabulary and now lives in +// `wire/rev2025-11-25/schemas.ts`, which also exports the era's full wire +// role unions. The deprecated Task* types remain importable from the types +// barrel (Q1-SD2); they appear in no role aggregate and no API signature. export const ClientRequestSchema = z.union([ PingRequestSchema, InitializeRequestSchema, @@ -2194,19 +2025,14 @@ export const ClientRequestSchema = z.union([ SubscribeRequestSchema, UnsubscribeRequestSchema, CallToolRequestSchema, - ListToolsRequestSchema, - GetTaskRequestSchema, - GetTaskPayloadRequestSchema, - ListTasksRequestSchema, - CancelTaskRequestSchema + ListToolsRequestSchema ]); export const ClientNotificationSchema = z.union([ CancelledNotificationSchema, ProgressNotificationSchema, InitializedNotificationSchema, - RootsListChangedNotificationSchema, - TaskStatusNotificationSchema + RootsListChangedNotificationSchema ]); export const ClientResultSchema = z.union([ @@ -2214,23 +2040,11 @@ export const ClientResultSchema = z.union([ CreateMessageResultSchema, CreateMessageResultWithToolsSchema, ElicitResultSchema, - ListRootsResultSchema, - GetTaskResultSchema, - ListTasksResultSchema, - CreateTaskResultSchema + ListRootsResultSchema ]); /* Server messages */ -export const ServerRequestSchema = z.union([ - PingRequestSchema, - CreateMessageRequestSchema, - ElicitRequestSchema, - ListRootsRequestSchema, - GetTaskRequestSchema, - GetTaskPayloadRequestSchema, - ListTasksRequestSchema, - CancelTaskRequestSchema -]); +export const ServerRequestSchema = z.union([PingRequestSchema, CreateMessageRequestSchema, ElicitRequestSchema, ListRootsRequestSchema]); export const ServerNotificationSchema = z.union([ CancelledNotificationSchema, @@ -2240,7 +2054,6 @@ export const ServerNotificationSchema = z.union([ ResourceListChangedNotificationSchema, ToolListChangedNotificationSchema, PromptListChangedNotificationSchema, - TaskStatusNotificationSchema, ElicitationCompleteNotificationSchema ]); @@ -2254,93 +2067,5 @@ export const ServerResultSchema = z.union([ ListResourceTemplatesResultSchema, ReadResourceResultSchema, CallToolResultSchema, - ListToolsResultSchema, - GetTaskResultSchema, - ListTasksResultSchema, - CreateTaskResultSchema + ListToolsResultSchema ]); - -/* Runtime schema lookup — result schemas by method */ -const resultSchemas: Record = { - ping: EmptyResultSchema, - initialize: InitializeResultSchema, - 'completion/complete': CompleteResultSchema, - 'logging/setLevel': EmptyResultSchema, - 'prompts/get': GetPromptResultSchema, - 'prompts/list': ListPromptsResultSchema, - 'resources/list': ListResourcesResultSchema, - 'resources/templates/list': ListResourceTemplatesResultSchema, - 'resources/read': ReadResourceResultSchema, - 'resources/subscribe': EmptyResultSchema, - 'resources/unsubscribe': EmptyResultSchema, - 'tools/call': z.union([CallToolResultSchema, CreateTaskResultSchema]), - 'tools/list': ListToolsResultSchema, - 'sampling/createMessage': z.union([CreateMessageResultWithToolsSchema, CreateTaskResultSchema]), - 'elicitation/create': z.union([ElicitResultSchema, CreateTaskResultSchema]), - 'roots/list': ListRootsResultSchema, - 'tasks/get': GetTaskResultSchema, - 'tasks/result': ResultSchema, - 'tasks/list': ListTasksResultSchema, - 'tasks/cancel': CancelTaskResultSchema -}; - -/** - * Gets the Zod schema for validating results of a given request method. - * Returns `undefined` for non-spec methods. - * @see getRequestSchema for explanation of the internal type assertion. - */ -export function getResultSchema(method: M): z.ZodType; -export function getResultSchema(method: string): z.ZodType | undefined; -export function getResultSchema(method: string): z.ZodType | undefined { - return resultSchemas[method as RequestMethod] as unknown as z.ZodType | undefined; -} - -/* Runtime schema lookup — request schemas by method */ -type RequestSchemaType = (typeof ClientRequestSchema.options)[number] | (typeof ServerRequestSchema.options)[number]; -type NotificationSchemaType = (typeof ClientNotificationSchema.options)[number] | (typeof ServerNotificationSchema.options)[number]; - -function buildSchemaMap(schemas: readonly T[]): Record { - const map: Record = {}; - for (const schema of schemas) { - const method = schema.shape.method.value; - map[method] = schema; - } - return map; -} - -const requestSchemas = buildSchemaMap([...ClientRequestSchema.options, ...ServerRequestSchema.options] as const) as Record< - RequestMethod, - RequestSchemaType ->; -const notificationSchemas = buildSchemaMap([...ClientNotificationSchema.options, ...ServerNotificationSchema.options] as const) as Record< - NotificationMethod, - NotificationSchemaType ->; - -/** - * Gets the Zod schema for a given request method. - * Returns `undefined` for non-spec methods. - * The return type is a ZodType that parses to RequestTypeMap[M], allowing callers - * to use schema.parse() without needing additional type assertions. - * - * Note: The internal cast is necessary because TypeScript can't correlate the - * Record-based schema lookup with the MethodToTypeMap-based RequestTypeMap - * when M is a generic type parameter. Both compute to the same type at - * instantiation, but TypeScript can't prove this statically. - */ -export function getRequestSchema(method: M): z.ZodType; -export function getRequestSchema(method: string): z.ZodType | undefined; -export function getRequestSchema(method: string): z.ZodType | undefined { - return requestSchemas[method as RequestMethod] as unknown as z.ZodType | undefined; -} - -/** - * Gets the Zod schema for a given notification method. - * Returns `undefined` for non-spec methods. - * @see getRequestSchema for explanation of the internal type assertion. - */ -export function getNotificationSchema(method: M): z.ZodType; -export function getNotificationSchema(method: string): z.ZodType | undefined; -export function getNotificationSchema(method: string): z.ZodType | undefined { - return notificationSchemas[method as NotificationMethod] as unknown as z.ZodType | undefined; -} diff --git a/packages/core/src/types/spec.types.2026-07-28.ts b/packages/core/src/types/spec.types.2026-07-28.ts index 7305df0462..1b222b9896 100644 --- a/packages/core/src/types/spec.types.2026-07-28.ts +++ b/packages/core/src/types/spec.types.2026-07-28.ts @@ -3,7 +3,7 @@ * * Source: https://github.com/modelcontextprotocol/modelcontextprotocol * Pulled from: https://raw.githubusercontent.com/modelcontextprotocol/modelcontextprotocol/main/schema/draft/schema.ts - * Last updated from commit: 9d700ed62dcf86cb77475c9b81930611a9182f46 + * Last updated from commit: 77cb26481e439d3437bc2bd6ccd19fcae86bb1ec * * DO NOT EDIT THIS FILE MANUALLY. Changes will be overwritten by automated updates. * To update this file, run: pnpm run fetch:spec-types 2026-07-28 @@ -569,7 +569,7 @@ export interface DiscoverRequest extends JSONRPCRequest { * * @category `server/discover` */ -export interface DiscoverResult extends Result { +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. @@ -674,6 +674,9 @@ export interface ClientCapabilities { * (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} */ @@ -768,6 +771,9 @@ export interface ServerCapabilities { * (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} */ @@ -2963,6 +2969,18 @@ export interface ElicitResult { content?: { [key: string]: string | number | boolean | string[] }; } +/** + * Parameters for a {@link ElicitationCompleteNotification | notifications/elicitation/complete} notification. + * + * @category `notifications/elicitation/complete` + */ +export interface ElicitationCompleteNotificationParams extends NotificationParams { + /** + * The ID of the elicitation that completed. + */ + elicitationId: string; +} + /** * An optional notification from the server to the client, informing it of a completion of a out-of-band elicitation request. * @@ -2973,12 +2991,7 @@ export interface ElicitResult { */ export interface ElicitationCompleteNotification extends JSONRPCNotification { method: 'notifications/elicitation/complete'; - params: { - /** - * The ID of the elicitation that completed. - */ - elicitationId: string; - }; + params: ElicitationCompleteNotificationParams; } /* Client messages */ diff --git a/packages/core/src/types/specTypeSchema.ts b/packages/core/src/types/specTypeSchema.ts index e538da8fa5..de66e99418 100644 --- a/packages/core/src/types/specTypeSchema.ts +++ b/packages/core/src/types/specTypeSchema.ts @@ -41,8 +41,6 @@ const SPEC_SCHEMA_KEYS = [ 'CallToolResultSchema', 'CancelledNotificationSchema', 'CancelledNotificationParamsSchema', - 'CancelTaskRequestSchema', - 'CancelTaskResultSchema', 'ClientCapabilitiesSchema', 'ClientNotificationSchema', 'ClientRequestSchema', @@ -56,7 +54,6 @@ const SPEC_SCHEMA_KEYS = [ 'CreateMessageRequestParamsSchema', 'CreateMessageResultSchema', 'CreateMessageResultWithToolsSchema', - 'CreateTaskResultSchema', 'CursorSchema', 'DiscoverRequestSchema', 'DiscoverResultSchema', @@ -73,10 +70,6 @@ const SPEC_SCHEMA_KEYS = [ 'GetPromptRequestSchema', 'GetPromptRequestParamsSchema', 'GetPromptResultSchema', - 'GetTaskPayloadRequestSchema', - 'GetTaskPayloadResultSchema', - 'GetTaskRequestSchema', - 'GetTaskResultSchema', 'IconSchema', 'IconsSchema', 'ImageContentSchema', @@ -103,8 +96,6 @@ const SPEC_SCHEMA_KEYS = [ 'ListResourceTemplatesResultSchema', 'ListRootsRequestSchema', 'ListRootsResultSchema', - 'ListTasksRequestSchema', - 'ListTasksResultSchema', 'ListToolsRequestSchema', 'ListToolsResultSchema', 'LoggingLevelSchema', @@ -135,7 +126,6 @@ const SPEC_SCHEMA_KEYS = [ 'RelatedTaskMetadataSchema', 'RequestSchema', 'RequestIdSchema', - 'RequestMetaEnvelopeSchema', 'RequestMetaSchema', 'ResourceSchema', 'ResourceContentsSchema', @@ -163,13 +153,8 @@ const SPEC_SCHEMA_KEYS = [ 'StringSchemaSchema', 'SubscribeRequestSchema', 'SubscribeRequestParamsSchema', - 'TaskSchema', 'TaskAugmentedRequestParamsSchema', - 'TaskCreationParamsSchema', 'TaskMetadataSchema', - 'TaskStatusSchema', - 'TaskStatusNotificationSchema', - 'TaskStatusNotificationParamsSchema', 'TextContentSchema', 'TextResourceContentsSchema', 'TitledMultiSelectEnumSchemaSchema', @@ -223,7 +208,12 @@ export type SpecTypeName = StripSchemaSuffix; /** * Maps each {@linkcode SpecTypeName} to its TypeScript type. * - * `SpecTypes['CallToolResult']` is equivalent to importing the `CallToolResult` type directly. + * `SpecTypes['Tool']` is equivalent to importing the `Tool` type directly. + * These validators cover the NEUTRAL model — the consumer-facing shapes with + * no wire-only members (`resultType`, the reserved `_meta` envelope keys). + * Per-revision WIRE validators are deliberately not public surface; they are + * planned to return as versioned `zod-schemas/` exports for + * consumers who validate raw wire traffic themselves. */ export type SpecTypes = { [K in SchemaKey as StripSchemaSuffix]: SchemaFor extends z.ZodType ? z.output> : never; diff --git a/packages/core/src/types/types.ts b/packages/core/src/types/types.ts index e0fe28b500..7c8f0d30a2 100644 --- a/packages/core/src/types/types.ts +++ b/packages/core/src/types/types.ts @@ -4,6 +4,27 @@ import type * as z from 'zod/v4'; +// Wire-module schema imports, TYPE-ONLY (erased at runtime): the deprecated +// task vocabulary and the per-request envelope are wire-era artifacts whose +// schemas live in the codec modules; their inferred TYPES stay importable +// from this neutral layer (Q1-SD2). +import type { + CancelTaskRequestSchema, + CancelTaskResultSchema, + CreateTaskResultSchema, + GetTaskPayloadRequestSchema, + GetTaskPayloadResultSchema, + GetTaskRequestSchema, + GetTaskResultSchema, + ListTasksRequestSchema, + ListTasksResultSchema, + TaskCreationParamsSchema, + TaskSchema, + TaskStatusNotificationParamsSchema, + TaskStatusNotificationSchema, + TaskStatusSchema +} from '../wire/rev2025-11-25/schemas.js'; +import type { RequestMetaEnvelopeSchema } from '../wire/rev2026-07-28/schemas.js'; import type { INTERNAL_ERROR, INVALID_PARAMS, INVALID_REQUEST, METHOD_NOT_FOUND, PARSE_ERROR } from './constants.js'; import type { AnnotationsSchema, @@ -17,8 +38,6 @@ import type { CallToolResultSchema, CancelledNotificationParamsSchema, CancelledNotificationSchema, - CancelTaskRequestSchema, - CancelTaskResultSchema, ClientCapabilitiesSchema, ClientNotificationSchema, ClientRequestSchema, @@ -32,7 +51,6 @@ import type { CreateMessageRequestSchema, CreateMessageResultSchema, CreateMessageResultWithToolsSchema, - CreateTaskResultSchema, CursorSchema, DiscoverRequestSchema, DiscoverResultSchema, @@ -49,10 +67,6 @@ import type { GetPromptRequestParamsSchema, GetPromptRequestSchema, GetPromptResultSchema, - GetTaskPayloadRequestSchema, - GetTaskPayloadResultSchema, - GetTaskRequestSchema, - GetTaskResultSchema, IconSchema, IconsSchema, ImageContentSchema, @@ -62,10 +76,8 @@ import type { InitializeRequestSchema, InitializeResultSchema, JSONRPCErrorResponseSchema, - JSONRPCMessageSchema, JSONRPCNotificationSchema, JSONRPCRequestSchema, - JSONRPCResponseSchema, JSONRPCResultResponseSchema, LegacyTitledEnumSchemaSchema, ListPromptsRequestSchema, @@ -76,8 +88,6 @@ import type { ListResourceTemplatesResultSchema, ListRootsRequestSchema, ListRootsResultSchema, - ListTasksRequestSchema, - ListTasksResultSchema, ListToolsRequestSchema, ListToolsResultSchema, LoggingLevelSchema, @@ -108,7 +118,6 @@ import type { ReadResourceResultSchema, RelatedTaskMetadataSchema, RequestIdSchema, - RequestMetaEnvelopeSchema, RequestMetaSchema, RequestSchema, ResourceContentsSchema, @@ -138,12 +147,7 @@ import type { SubscribeRequestParamsSchema, SubscribeRequestSchema, TaskAugmentedRequestParamsSchema, - TaskCreationParamsSchema, TaskMetadataSchema, - TaskSchema, - TaskStatusNotificationParamsSchema, - TaskStatusNotificationSchema, - TaskStatusSchema, TextContentSchema, TextResourceContentsSchema, TitledMultiSelectEnumSchemaSchema, @@ -186,21 +190,41 @@ type Flatten = T extends Primitive type Infer = Flatten>; +/** + * Wire-only members hidden from the public types. + * + * `resultType` is the protocol-revision-2026-07-28 wire discrimination field + * on results. It is consumed by the SDK's protocol layer (and stripped before + * results reach consumers), so the public result types do not declare it. + * The wire schemas continue to model it internally. + */ +type WireOnlyResultKey = 'resultType'; + +/** + * Removes wire-only members from a (possibly union) schema-inferred type + * while preserving every other declared member, optionality, and the loose + * index signature. + */ +type StripWireOnly = T extends unknown ? { [K in keyof T as K extends WireOnlyResultKey ? never : K]: T[K] } : never; + /* JSON-RPC types */ export type ProgressToken = Infer; export type Cursor = Infer; export type Request = Infer; +/** @deprecated 2025-11-25 wire vocabulary with no SDK runtime; kept importable for interoperability only. */ export type TaskAugmentedRequestParams = Infer; export type RequestMeta = Infer; export type Notification = Infer; -export type Result = Infer; +export type Result = StripWireOnly>; export type RequestId = Infer; export type JSONRPCRequest = Infer; export type JSONRPCNotification = Infer; -export type JSONRPCResponse = Infer; export type JSONRPCErrorResponse = Infer; -export type JSONRPCResultResponse = Infer; -export type JSONRPCMessage = Infer; +// The response/message envelopes embed result objects, so they are rebuilt +// from the public (wire-only-stripped) `Result` rather than schema-inferred. +export type JSONRPCResultResponse = Omit, 'result'> & { result: Result }; +export type JSONRPCResponse = JSONRPCResultResponse | JSONRPCErrorResponse; +export type JSONRPCMessage = JSONRPCRequest | JSONRPCNotification | JSONRPCResultResponse | JSONRPCErrorResponse; export type RequestParams = Infer; export type NotificationParams = Infer; /** @@ -210,7 +234,7 @@ export type NotificationParams = Infer; export type RequestMetaEnvelope = Infer; /* Empty result */ -export type EmptyResult = Infer; +export type EmptyResult = StripWireOnly>; /* Cancellation */ export type CancelledNotificationParams = Infer; @@ -243,12 +267,12 @@ export type InitializeRequest = Infer; * months. See `ServerCapabilitiesSchema`. */ export type ServerCapabilities = Infer; -export type InitializeResult = Infer; +export type InitializeResult = StripWireOnly>; export type InitializedNotification = Infer; /* Discovery */ export type DiscoverRequest = Infer; -export type DiscoverResult = Infer; +export type DiscoverResult = StripWireOnly>; /* Ping */ export type PingRequest = Infer; @@ -258,28 +282,52 @@ export type Progress = Infer; export type ProgressNotificationParams = Infer; export type ProgressNotification = Infer; -/* Tasks */ +/* Tasks + * + * The task wire surface defined by the 2025-11-25 protocol revision. These + * types stay importable as wire vocabulary for interoperability with peers on + * that revision, but they appear in no SDK API signature: the SDK has no task + * runtime, and the typed method maps (RequestMethod/RequestTypeMap/ + * ResultTypeMap/NotificationTypeMap) do not include the task methods. + * Removable at the major version that drops 2025-era support. + */ +/** @deprecated 2025-11-25 wire vocabulary with no SDK runtime; kept importable for interoperability only. */ export type Task = Infer; +/** @deprecated 2025-11-25 wire vocabulary with no SDK runtime; kept importable for interoperability only. */ export type TaskStatus = Infer; +/** @deprecated 2025-11-25 wire vocabulary with no SDK runtime; kept importable for interoperability only. */ export type TaskCreationParams = Infer; +/** @deprecated 2025-11-25 wire vocabulary with no SDK runtime; kept importable for interoperability only. */ export type TaskMetadata = Infer; +/** @deprecated 2025-11-25 wire vocabulary with no SDK runtime; kept importable for interoperability only. */ export type RelatedTaskMetadata = Infer; -export type CreateTaskResult = Infer; +/** @deprecated 2025-11-25 wire vocabulary with no SDK runtime; kept importable for interoperability only. */ +export type CreateTaskResult = StripWireOnly>; +/** @deprecated 2025-11-25 wire vocabulary with no SDK runtime; kept importable for interoperability only. */ export type TaskStatusNotificationParams = Infer; +/** @deprecated 2025-11-25 wire vocabulary with no SDK runtime; kept importable for interoperability only. */ export type TaskStatusNotification = Infer; +/** @deprecated 2025-11-25 wire vocabulary with no SDK runtime; kept importable for interoperability only. */ export type GetTaskRequest = Infer; -export type GetTaskResult = Infer; +/** @deprecated 2025-11-25 wire vocabulary with no SDK runtime; kept importable for interoperability only. */ +export type GetTaskResult = StripWireOnly>; +/** @deprecated 2025-11-25 wire vocabulary with no SDK runtime; kept importable for interoperability only. */ export type GetTaskPayloadRequest = Infer; +/** @deprecated 2025-11-25 wire vocabulary with no SDK runtime; kept importable for interoperability only. */ export type ListTasksRequest = Infer; -export type ListTasksResult = Infer; +/** @deprecated 2025-11-25 wire vocabulary with no SDK runtime; kept importable for interoperability only. */ +export type ListTasksResult = StripWireOnly>; +/** @deprecated 2025-11-25 wire vocabulary with no SDK runtime; kept importable for interoperability only. */ export type CancelTaskRequest = Infer; -export type CancelTaskResult = Infer; -export type GetTaskPayloadResult = Infer; +/** @deprecated 2025-11-25 wire vocabulary with no SDK runtime; kept importable for interoperability only. */ +export type CancelTaskResult = StripWireOnly>; +/** @deprecated 2025-11-25 wire vocabulary with no SDK runtime; kept importable for interoperability only. */ +export type GetTaskPayloadResult = StripWireOnly>; /* Pagination */ export type PaginatedRequestParams = Infer; export type PaginatedRequest = Infer; -export type PaginatedResult = Infer; +export type PaginatedResult = StripWireOnly>; /* Resources */ export type ResourceContents = Infer; @@ -289,13 +337,13 @@ export type Resource = Infer; // TODO: Overlaps with exported `ResourceTemplate` class from `server`. export type ResourceTemplateType = Infer; export type ListResourcesRequest = Infer; -export type ListResourcesResult = Infer; +export type ListResourcesResult = StripWireOnly>; export type ListResourceTemplatesRequest = Infer; -export type ListResourceTemplatesResult = Infer; +export type ListResourceTemplatesResult = StripWireOnly>; export type ResourceRequestParams = Infer; export type ReadResourceRequestParams = Infer; export type ReadResourceRequest = Infer; -export type ReadResourceResult = Infer; +export type ReadResourceResult = StripWireOnly>; export type ResourceListChangedNotification = Infer; export type SubscribeRequestParams = Infer; export type SubscribeRequest = Infer; @@ -308,7 +356,7 @@ export type ResourceUpdatedNotification = Infer; export type Prompt = Infer; export type ListPromptsRequest = Infer; -export type ListPromptsResult = Infer; +export type ListPromptsResult = StripWireOnly>; export type GetPromptRequestParams = Infer; export type GetPromptRequest = Infer; export type TextContent = Infer; @@ -320,7 +368,7 @@ export type EmbeddedResource = Infer; export type ResourceLink = Infer; export type ContentBlock = Infer; export type PromptMessage = Infer; -export type GetPromptResult = Infer; +export type GetPromptResult = StripWireOnly>; export type PromptListChangedNotification = Infer; /* Tools */ @@ -328,10 +376,10 @@ export type ToolAnnotations = Infer; export type ToolExecution = Infer; export type Tool = Infer; export type ListToolsRequest = Infer; -export type ListToolsResult = Infer; +export type ListToolsResult = StripWireOnly>; export type CallToolRequestParams = Infer; -export type CallToolResult = Infer; -export type CompatibilityCallToolResult = Infer; +export type CallToolResult = StripWireOnly>; +export type CompatibilityCallToolResult = StripWireOnly>; export type CallToolRequest = Infer; export type ToolListChangedNotification = Infer; @@ -351,8 +399,8 @@ export type SamplingMessageContentBlock = Infer; export type CreateMessageRequestParams = Infer; export type CreateMessageRequest = Infer; -export type CreateMessageResult = Infer; -export type CreateMessageResultWithTools = Infer; +export type CreateMessageResult = StripWireOnly>; +export type CreateMessageResultWithTools = StripWireOnly>; /* Elicitation */ export type BooleanSchema = Infer; @@ -373,39 +421,48 @@ export type ElicitRequestURLParams = Infer; export type ElicitRequest = Infer; export type ElicitationCompleteNotificationParams = Infer; export type ElicitationCompleteNotification = Infer; -export type ElicitResult = Infer; +export type ElicitResult = StripWireOnly>; /* Autocomplete */ export type ResourceTemplateReference = Infer; export type PromptReference = Infer; export type CompleteRequestParams = Infer; export type CompleteRequest = Infer; -export type CompleteResult = Infer; +export type CompleteResult = StripWireOnly>; /* Roots */ export type Root = Infer; export type ListRootsRequest = Infer; -export type ListRootsResult = Infer; +export type ListRootsResult = StripWireOnly>; export type RootsListChangedNotification = Infer; /* Client messages */ export type ClientRequest = Infer; export type ClientNotification = Infer; -export type ClientResult = Infer; +export type ClientResult = StripWireOnly>; /* Server messages */ export type ServerRequest = Infer; export type ServerNotification = Infer; -export type ServerResult = Infer; +export type ServerResult = StripWireOnly>; /* Protocol type maps */ type MethodToTypeMap = { [T in U as T extends { method: infer M extends string } ? M : never]: T; }; -export type RequestMethod = ClientRequest['method'] | ServerRequest['method']; -export type NotificationMethod = ClientNotification['method'] | ServerNotification['method']; -export type RequestTypeMap = MethodToTypeMap; -export type NotificationTypeMap = MethodToTypeMap; +/** + * Task methods are 2025-11-25 wire vocabulary with no SDK runtime: the task + * wire types stay importable (see the Tasks section above), but the typed + * method surface — `request()`, `setRequestHandler()`, `ctx.mcpReq.send()` — + * does not offer them. The wire schemas keep parsing task vocabulary for + * interoperability with 2025-11-25 peers. + */ +type TaskRequestMethod = 'tasks/get' | 'tasks/result' | 'tasks/list' | 'tasks/cancel'; +type TaskNotificationMethod = 'notifications/tasks/status'; +export type RequestMethod = Exclude; +export type NotificationMethod = Exclude; +export type RequestTypeMap = MethodToTypeMap>; +export type NotificationTypeMap = MethodToTypeMap>; export type ResultTypeMap = { ping: EmptyResult; initialize: InitializeResult; @@ -418,15 +475,11 @@ export type ResultTypeMap = { 'resources/read': ReadResourceResult; 'resources/subscribe': EmptyResult; 'resources/unsubscribe': EmptyResult; - 'tools/call': CallToolResult | CreateTaskResult; + 'tools/call': CallToolResult; 'tools/list': ListToolsResult; - 'sampling/createMessage': CreateMessageResult | CreateMessageResultWithTools | CreateTaskResult; - 'elicitation/create': ElicitResult | CreateTaskResult; + 'sampling/createMessage': CreateMessageResult | CreateMessageResultWithTools; + 'elicitation/create': ElicitResult; 'roots/list': ListRootsResult; - 'tasks/get': GetTaskResult; - 'tasks/result': Result; - 'tasks/list': ListTasksResult; - 'tasks/cancel': CancelTaskResult; }; /** @@ -555,6 +608,38 @@ export type ListChangedHandlers = { resources?: ListChangedOptions; }; +/** + * Protocol-era classification of an inbound message. + * + * Populated by transports that classify messages at the edge (e.g. an HTTP + * entry distinguishing 2025-era from 2026-era traffic). The wire era itself + * is connection state (the negotiated protocol version held by the + * `Client`/`Server` instance); the protocol layer validates a classified + * message against that instance era at dispatch — a mismatch is treated as + * an entry/routing error, never a per-message era switch. Unclassified + * traffic is dispatched on the instance era unchanged. + */ +export interface MessageClassification { + /** + * The wire era the message was classified into: `legacy` for the + * 2025-11-25 family of revisions, `modern` for 2026-07-28 and later. + */ + era: 'legacy' | 'modern'; + + /** + * The exact protocol revision, when the classifier derived one. + */ + revision?: string; + + /** + * The per-request `_meta` envelope, when the classifier extracted it. + * Partial: whichever reserved keys the message actually carried — + * envelope requiredness is enforced per request at dispatch time, not at + * the classifying edge. + */ + envelope?: Partial; +} + /** * Extra information about a message. */ @@ -564,6 +649,14 @@ export interface MessageExtraInfo { */ request?: globalThis.Request; + /** + * Protocol-era classification of the message, when the transport + * classified it at the edge. Validated by the protocol layer against the + * instance's negotiated era at dispatch (the edge→instance handoff + * check); it does not select the era itself. + */ + classification?: MessageClassification; + /** * The authentication information. */ diff --git a/packages/core/src/wire/bootstrap.ts b/packages/core/src/wire/bootstrap.ts new file mode 100644 index 0000000000..3f54029328 --- /dev/null +++ b/packages/core/src/wire/bootstrap.ts @@ -0,0 +1,40 @@ +/** + * Static era pins for lifecycle messages on the OUTBOUND path (the + * chicken-and-egg bootstrap): these messages are sent before any negotiated + * version exists, and they self-identify their era by construction — + * `initialize`/`notifications/initialized` ARE the legacy handshake (Q2: + * `initialize` ⇒ legacy), and `server/discover` exists only on the 2026 era. + * No negotiated-state guess ever picks a payload schema for them. + * + * Scope notes: + * - OUTBOUND ONLY. Inbound era truth is per-request classification (Q2) with + * session state as fallback — pinning inbound would override the + * classifier (an unclassified `server/discover` request classifies legacy + * and correctly falls to −32601 by registry absence). + * - `ping` is deliberately NOT pinned. A bare `{method: 'ping'}` carries no + * era marker — under Q2 it classifies legacy by DEFAULT, not by + * self-identification — and pinning it would let a negotiated-modern + * session emit a 2025-only method onto the modern leg (the exact inverse + * leak registry membership exists to prevent). `ping` era-gates like any + * other method: present on the 2025 era, absent from the 2026 era (the + * modern keepalive story is owned by the negotiation milestones). + */ +import type { WireCodec } from './codec.js'; +import { codecForVersion, MODERN_WIRE_REVISION } from './codec.js'; + +export function bootstrapOutboundCodec(method: string): WireCodec | undefined { + switch (method) { + case 'initialize': + case 'notifications/initialized': { + // The legacy handshake, by definition (Q2). + return codecForVersion(undefined); + } + case 'server/discover': { + // The modern discovery exchange, 2026-era only. + return codecForVersion(MODERN_WIRE_REVISION); + } + default: { + return undefined; + } + } +} diff --git a/packages/core/src/wire/codec.ts b/packages/core/src/wire/codec.ts new file mode 100644 index 0000000000..586cb2e044 --- /dev/null +++ b/packages/core/src/wire/codec.ts @@ -0,0 +1,203 @@ +/** + * The era-granular wire-codec layer (Q1 increment 2). + * + * The SDK separates a revision-neutral model layer (the public types — no + * `resultType`, no `_meta` envelope keys, no retry fields) from per-revision + * WIRE CODECS that own revision-exact schemas, method registries, and the + * decode (wire → neutral lift) / encode (neutral → wire stamp) transforms. + * The codec is a pure function of the negotiated protocol version, which is + * ordinary connection state on the `Protocol` instance: the client stores it + * when its handshake completes, the server stores it at `_oninitialize` (and + * modern-era server instances get it set at instance binding by the entry). + * There is no side table — era resolution is `codecForVersion()`, with the pre-negotiation window covered by the outbound method + * pins in `bootstrap.ts`. + * + * REQUIRED DISCLOSURE (Q1-SD1, era granularity): "the negotiated version + * determines which types are serialized/deserialized over the wire" cashes + * out as "the negotiated wire ERA determines them". All five legacy protocol + * versions (2024-10-07 … 2025-11-25) share one wire vocabulary and map to the + * single 2025-era codec — exactly how the single schema set already served + * all five — and '2026-07-28' maps to the 2026-era codec. A new codec exists + * only when wire vocabulary actually diverges; intra-era vocabulary is NOT + * keyed by exact version. + * + * Deletions are physical: registry membership is the deletion story. The + * 2026-era registry has no `tasks/*`, `initialize`, `ping`, `logging/setLevel`, + * `resources/(un)subscribe` or server→client wire-request entries, so an + * inbound era-mismatched method falls to −32601 by absence — even when a + * handler is registered — and an outbound one dies locally with a typed + * `SdkError` before anything reaches the transport. The 2025-era registry has + * no `server/discover`/`subscriptions/listen`/MRTR entries, symmetrically. + * + * Custom-handler shadowing policy (both directions): a method that belongs to + * the SPEC-METHOD UNIVERSE — the union of every codec's registry, derived, + * not hand-curated — is ALWAYS era-gated, so a custom handler registered for + * a deleted spec method (e.g. `tasks/get`) serves it only on the era that + * defines it. Methods outside the universe are consumer-owned extension + * methods: they are era-blind and require explicit schemas, exactly as today. + * + * Everything in `wire/` is internal to the bundled, `private: true` core — + * nothing per-revision is public surface, and nothing here may ever be + * exported from `core/public`. + */ +import type * as z from 'zod/v4'; + +import type { SdkError } from '../errors/sdkErrors.js'; +import type { + MessageClassification, + NotificationMethod, + NotificationTypeMap, + RequestMetaEnvelope, + RequestMethod, + RequestTypeMap, + Result, + ResultTypeMap +} from '../types/types.js'; +import { rev2025Codec } from './rev2025-11-25/codec.js'; +import { rev2026Codec } from './rev2026-07-28/codec.js'; + +/** Wire eras with distinct vocabulary. */ +export type WireEra = '2025-11-25' | '2026-07-28'; + +/** + * The modern wire revision literal. Internal only — deliberately NOT a public + * constant (G-D2-4: no public modern-version constant ships before era-aware + * list semantics exist). + */ +export const MODERN_WIRE_REVISION = '2026-07-28'; + +/** + * Wire-only material lifted off an inbound message by the protocol layer + * before dispatch (the V-3 seam): the reserved `_meta` envelope keys and the + * multi-round-trip driver fields. This is the typed driver-material channel + * of the codec contract — handlers never see it; the protocol layer surfaces + * it via `ctx.mcpReq.envelope` / `.inputResponses` / `.requestState`, and the + * MRTR driver (M4.1) consumes the retry fields from here. + */ +export interface LiftedWireMaterial { + // Partial: the lift surfaces whichever reserved keys the message actually + // carried — a peer on an adjacent revision may legally send a subset, and + // envelope requiredness is enforced per request at dispatch time + // (`checkInboundEnvelope`), not by the lift. + envelope?: Partial; + inputResponses?: Record; + requestState?: string; +} + +/** Result decode outcomes — the raw-first discrimination (V-1) lives in `decodeResult`. */ +export type DecodedResult = + | { + kind: 'complete'; + /** The neutral result value: wire-only material consumed/stripped. */ + result: Result; + } + | { + kind: 'input_required'; + /** + * Driver-only material (never consumer-visible). The full + * multi-round-trip driver is M4.1 scope; this seam carries the + * discriminated payload to it. + */ + inputRequests: Record; + requestState?: string; + } + | { kind: 'invalid'; error: SdkError }; + +/** + * The per-era wire codec contract (design C §3, adapted to the live funnel + * layout: the universal wire-only LIFT runs once in the protocol layer for + * every message — spec, custom, and fallback paths alike — and codecs consume + * the lifted material rather than re-implementing the strip per era). + */ +export interface WireCodec { + readonly era: WireEra; + + /** Registry membership — the deletion story (inbound −32601 by absence; outbound typed local error). */ + hasRequestMethod(method: string): boolean; + hasNotificationMethod(method: string): boolean; + + /** + * Era-exact dispatch schemas, resolved at dispatch time (never at + * registration time). The method-literal overloads carry the typed parse + * result for statically known spec methods, so call sites need no type + * assertion; `undefined` means the method has no entry on this era's + * registry. + */ + requestSchema(method: M): z.ZodType | undefined; + requestSchema(method: string): z.ZodType | undefined; + resultSchema(method: M): z.ZodType | undefined; + resultSchema(method: string): z.ZodType | undefined; + notificationSchema(method: M): z.ZodType | undefined; + notificationSchema(method: string): z.ZodType | undefined; + + /** + * Step 1 of result decoding: RAW `resultType` handling BEFORE any schema + * validation (V-1's structural home). Era postures (Q1-SD3): + * - 2026 era: required discriminator — absent ⇒ typed error naming the + * spec violation; `input_required` ⇒ driver payload; unknown ⇒ invalid, + * no retry; `complete` ⇒ consume + lift. + * - 2025 era: `resultType` is foreign vocabulary ⇒ strip-on-lift. + */ + decodeResult(method: string, raw: unknown): DecodedResult; + + /** + * Outbound result mapping (the stamp seam). The 2025-era codec is the + * identity — it has NO stamp code path (the never-stamp guarantee). The + * 2026-era codec stamps `resultType` and strictly enforces the 2026 wire + * shape for the known deleted-field set (`execution.taskSupport`, + * `capabilities.tasks` — Q1-SD3 iii). ttlMs/cacheScope stamping content + * is M3.2 scope and lands in this seam. + */ + encodeResult(method: string, result: Result): Result; + + /** + * Inbound envelope enforcement for era-classified traffic: validates the + * lifted envelope material of a request. Returns an error message when + * the era requires an envelope and it is missing/invalid (→ −32602 at the + * dispatch layer); `undefined` when acceptable. The 2025 era never + * requires an envelope. + */ + checkInboundEnvelope(material: LiftedWireMaterial): string | undefined; +} + +/** + * Era resolution, many-to-one (Q1-SD1): all `SUPPORTED_PROTOCOL_VERSIONS` + * (the five legacy versions) → the 2025-era codec; '2026-07-28' → the + * 2026-era codec; `undefined`/unknown → legacy (the DV-13 default posture — + * hand-constructed instances and unclassified traffic are legacy-era). + * + */ +export function codecForVersion(version: string | undefined): WireCodec { + return version === MODERN_WIRE_REVISION ? rev2026Codec : rev2025Codec; +} + +/** + * The wire era an edge classification names (Q2 — produced at the + * transport/entry edge; this layer only CONSUMES it). The dispatch funnel no + * longer resolves a codec FROM the classification: era is instance state, and + * a classified inbound message is VALIDATED against the instance era — a + * mismatch is an entry/routing error, never a per-message era switch. The + * exact `revision` wins over the coarse era flag when both are present. + */ +export function classifiedWireEra(classification: MessageClassification): WireEra { + if (classification.revision !== undefined) return codecForVersion(classification.revision).era; + return classification.era === 'modern' ? rev2026Codec.era : rev2025Codec.era; +} + +/** + * The derived spec-method universe: the union of every codec registry. A + * method in this set is era-gated at dispatch and send time; a method outside + * it is a consumer-owned extension method (era-blind, schema-explicit). + * Derived from the registries — never hand-curated (the LEGACY_ONLY_METHODS + * table class is exactly what registry membership replaces). + */ +export function isSpecRequestMethod(method: string): boolean { + return ALL_CODECS.some(codec => codec.hasRequestMethod(method)); +} + +export function isSpecNotificationMethod(method: string): boolean { + return ALL_CODECS.some(codec => codec.hasNotificationMethod(method)); +} + +const ALL_CODECS: readonly WireCodec[] = [rev2025Codec, rev2026Codec]; diff --git a/packages/core/src/wire/rev2025-11-25/codec.ts b/packages/core/src/wire/rev2025-11-25/codec.ts new file mode 100644 index 0000000000..458379d9cd --- /dev/null +++ b/packages/core/src/wire/rev2025-11-25/codec.ts @@ -0,0 +1,64 @@ +/** + * The 2025-era wire codec: decode/encode ≈ identity. + * + * This codec serves every legacy protocol version (2024-10-07 … 2025-11-25). + * It is BEHAVIOR-FROZEN behind the Q10-L2 byte-identity suite — its schemas + * are today's schemas, its registry is today's method map, and its encode + * path is the identity. + * + * Never-stamp guarantee: `encodeResult` is the identity function. There is no + * stamp code path in this module — a 2025-era response cannot carry + * `resultType`, `ttlMs`, `cacheScope`, or envelope keys because no code here + * can write them, not because a stamping branch is gated off. + * + * One deliberate exception to "no 2026 code path" (Q1-SD3 ii, amending the + * V-2 'no code path at all' design claim): `decodeResult` STRIPS a foreign + * `resultType` key from inbound results before validation (strip-on-lift). + * `resultType` is not 2025 vocabulary — a 2025 peer that sends it is + * misbehaving — and the ruled posture is tolerate-and-drop so the foreign key + * can neither surface to consumers (the neutral types have no slot for it) + * nor leak through the retained loose-object passthrough. This is the ONLY + * 2026-vocabulary code path in the 2025 codec, it exists on the decode side + * only, and it deletes — never reads, maps, or emits — the foreign value. + */ +import type { Result } from '../../types/types.js'; +import type { DecodedResult, LiftedWireMaterial, WireCodec } from '../codec.js'; +import { getNotificationSchema, getRequestSchema, getResultSchema, hasNotificationMethod2025, hasRequestMethod2025 } from './registry.js'; + +function isPlainObject(value: unknown): value is Record { + return value !== null && typeof value === 'object' && !Array.isArray(value); +} + +/** The wire→neutral trust boundary: a decoded 2025-era wire result is adopted as the neutral `Result` here (the module's single deliberate assertion). */ +function toNeutralResult(value: unknown): Result { + return value as Result; +} + +export const rev2025Codec: WireCodec = { + era: '2025-11-25', + + hasRequestMethod: hasRequestMethod2025, + hasNotificationMethod: hasNotificationMethod2025, + + requestSchema: getRequestSchema, + resultSchema: getResultSchema, + notificationSchema: getNotificationSchema, + + decodeResult(_method: string, raw: unknown): DecodedResult { + // Strip-on-lift (Q1-SD3 ii): a foreign `resultType` on the 2025 leg is + // dropped before validation, whatever its value. There is no + // discrimination on this era — `resultType` carries no meaning here. + if (isPlainObject(raw) && 'resultType' in raw) { + const stripped = { ...raw }; + delete stripped['resultType']; + return { kind: 'complete', result: toNeutralResult(stripped) }; + } + return { kind: 'complete', result: toNeutralResult(raw) }; + }, + + // The never-stamp guarantee: identity. No stamp code path exists. + encodeResult: (_method: string, result: Result): Result => result, + + // The 2025 era never requires a per-request envelope. + checkInboundEnvelope: (_material: LiftedWireMaterial): string | undefined => undefined +}; diff --git a/packages/core/src/wire/rev2025-11-25/registry.ts b/packages/core/src/wire/rev2025-11-25/registry.ts new file mode 100644 index 0000000000..e865fb58ea --- /dev/null +++ b/packages/core/src/wire/rev2025-11-25/registry.ts @@ -0,0 +1,213 @@ +/** + * The 2025-era method registries — re-homed verbatim from + * `types/schemas.ts` (Q1 increment-2 step 1: mechanical relocation behind the + * codec interface; the registry CONTENT is byte-identical to the pre-split + * maps and is pinned by reference in `test/types/registryPins.test.ts`). + * + * This era serves all five legacy protocol versions (2024-10-07 … + * 2025-11-25), exactly as the single schema set did before the split. It is + * BEHAVIOR-FROZEN behind the Q10-L2 byte-identity suite: the request and + * notification maps carry the full deliberate 2025-11-25 wire vocabulary, + * including the task family (the #2248 wire-interop restore). The RESULT map + * is the runtime/typed ALIGNED map (PR #2293 review): keyed by + * `RequestMethod` so it cannot drift from the typed `ResultTypeMap` — no + * task-result union members and no `tasks/*` entries; a task-capable 2025 + * peer's `CreateTaskResult` answer fails the plain per-method schema as a + * typed invalid-result error, and callers needing task interop pass an + * explicit result schema (see `test/shared/typedMapAlignment.test.ts`). + * + * 2026-only vocabulary (`server/discover`, `subscriptions/listen`, the MRTR + * shells, `resultType`, the `_meta` envelope) has NO entry and NO code path + * here — the inverse-leak guarantee is physical absence, not discipline. + */ +import type * as z from 'zod/v4'; + +import { + CallToolRequestSchema, + CallToolResultSchema, + CancelledNotificationSchema, + CompleteRequestSchema, + CompleteResultSchema, + CreateMessageRequestSchema, + CreateMessageResultWithToolsSchema, + ElicitationCompleteNotificationSchema, + ElicitRequestSchema, + ElicitResultSchema, + EmptyResultSchema, + GetPromptRequestSchema, + GetPromptResultSchema, + InitializedNotificationSchema, + InitializeRequestSchema, + InitializeResultSchema, + ListPromptsRequestSchema, + ListPromptsResultSchema, + ListResourcesRequestSchema, + ListResourcesResultSchema, + ListResourceTemplatesRequestSchema, + ListResourceTemplatesResultSchema, + ListRootsRequestSchema, + ListRootsResultSchema, + ListToolsRequestSchema, + ListToolsResultSchema, + LoggingMessageNotificationSchema, + PingRequestSchema, + ProgressNotificationSchema, + PromptListChangedNotificationSchema, + ReadResourceRequestSchema, + ReadResourceResultSchema, + ResourceListChangedNotificationSchema, + ResourceUpdatedNotificationSchema, + RootsListChangedNotificationSchema, + SetLevelRequestSchema, + SubscribeRequestSchema, + ToolListChangedNotificationSchema, + UnsubscribeRequestSchema +} from '../../types/schemas.js'; +import type { NotificationMethod, NotificationTypeMap, RequestMethod, RequestTypeMap, ResultTypeMap } from '../../types/types.js'; +import type { ClientNotificationSchema, ClientRequestSchema, ServerNotificationSchema, ServerRequestSchema } from './schemas.js'; +import { + CancelTaskRequestSchema, + GetTaskPayloadRequestSchema, + GetTaskRequestSchema, + ListTasksRequestSchema, + TaskStatusNotificationSchema +} from './schemas.js'; + +/* The era's wire vocabulary, derived from the wire role unions in + * `./schemas.ts` (the same unions the registries used to be built from at + * runtime). Keying the maps by these derived unions makes drift a compile + * error in BOTH directions: a union member without a map entry, a map entry + * the unions do not know, and an entry pointing at a different method's + * schema all fail to typecheck. */ +type WireRequest = z.output | z.output; +type WireNotification = z.output | z.output; + +/** Every request method in the 2025-era wire vocabulary (the typed `RequestMethod` surface plus the task family). */ +export type Rev2025RequestMethod = WireRequest['method']; +/** Every notification method in the 2025-era wire vocabulary. */ +export type Rev2025NotificationMethod = WireNotification['method']; + +/* Runtime schema lookup — result schemas by method */ +// Keyed by `RequestMethod` and valued by `z.ZodType` so the +// runtime map and the typed `ResultTypeMap` cannot drift: a missing entry, an +// extra key, or an entry that does not parse to the typed map's result type +// is a compile error. No entry may be looser than the typed map (no +// task-result union members) and no key may fall outside it (no `tasks/*` +// entries — the task methods are 2025-11-25 wire vocabulary with no SDK +// runtime; callers needing task interop pass an explicit schema). +const resultSchemas: { readonly [M in RequestMethod]: z.ZodType } = { + ping: EmptyResultSchema, + initialize: InitializeResultSchema, + 'completion/complete': CompleteResultSchema, + 'logging/setLevel': EmptyResultSchema, + 'prompts/get': GetPromptResultSchema, + 'prompts/list': ListPromptsResultSchema, + 'resources/list': ListResourcesResultSchema, + 'resources/templates/list': ListResourceTemplatesResultSchema, + 'resources/read': ReadResourceResultSchema, + 'resources/subscribe': EmptyResultSchema, + 'resources/unsubscribe': EmptyResultSchema, + 'tools/call': CallToolResultSchema, + 'tools/list': ListToolsResultSchema, + 'sampling/createMessage': CreateMessageResultWithToolsSchema, + 'elicitation/create': ElicitResultSchema, + 'roots/list': ListRootsResultSchema +}; + +/* Runtime schema lookup — request and notification schemas by method. + * + * The entries are the SAME schema objects the wire role unions are built + * from (reference identity is pinned by `test/types/registryPins.test.ts`), + * and the key order preserves the pre-split union iteration order so the + * exported method lists are byte-identical to the builder they replace. */ +const requestSchemas: { readonly [M in Rev2025RequestMethod]: z.ZodType> } = { + ping: PingRequestSchema, + initialize: InitializeRequestSchema, + 'completion/complete': CompleteRequestSchema, + 'logging/setLevel': SetLevelRequestSchema, + 'prompts/get': GetPromptRequestSchema, + 'prompts/list': ListPromptsRequestSchema, + 'resources/list': ListResourcesRequestSchema, + 'resources/templates/list': ListResourceTemplatesRequestSchema, + 'resources/read': ReadResourceRequestSchema, + 'resources/subscribe': SubscribeRequestSchema, + 'resources/unsubscribe': UnsubscribeRequestSchema, + 'tools/call': CallToolRequestSchema, + 'tools/list': ListToolsRequestSchema, + 'tasks/get': GetTaskRequestSchema, + 'tasks/result': GetTaskPayloadRequestSchema, + 'tasks/list': ListTasksRequestSchema, + 'tasks/cancel': CancelTaskRequestSchema, + 'sampling/createMessage': CreateMessageRequestSchema, + 'elicitation/create': ElicitRequestSchema, + 'roots/list': ListRootsRequestSchema +}; + +const notificationSchemas: { readonly [M in Rev2025NotificationMethod]: z.ZodType> } = { + 'notifications/cancelled': CancelledNotificationSchema, + 'notifications/progress': ProgressNotificationSchema, + 'notifications/initialized': InitializedNotificationSchema, + 'notifications/roots/list_changed': RootsListChangedNotificationSchema, + 'notifications/tasks/status': TaskStatusNotificationSchema, + 'notifications/message': LoggingMessageNotificationSchema, + 'notifications/resources/updated': ResourceUpdatedNotificationSchema, + 'notifications/resources/list_changed': ResourceListChangedNotificationSchema, + 'notifications/tools/list_changed': ToolListChangedNotificationSchema, + 'notifications/prompts/list_changed': PromptListChangedNotificationSchema, + 'notifications/elicitation/complete': ElicitationCompleteNotificationSchema +}; + +/** The 2025-era request-method set (registry membership = the deletion story). */ +export function hasRequestMethod2025(method: string): method is Rev2025RequestMethod { + return Object.prototype.hasOwnProperty.call(requestSchemas, method); +} + +/** The 2025-era notification-method set. */ +export function hasNotificationMethod2025(method: string): method is Rev2025NotificationMethod { + return Object.prototype.hasOwnProperty.call(notificationSchemas, method); +} + +/** Result-map membership: exactly the typed `RequestMethod` set (no task entries). */ +function hasResultMethod(method: string): method is RequestMethod { + return Object.prototype.hasOwnProperty.call(resultSchemas, method); +} + +/** + * Gets the Zod schema for validating results of a given request method. + * Returns `undefined` for non-spec methods. + * The typed overload is backed by the map's own typing (`z.ZodType` + * per entry), so callers with a statically known method can use the parsed + * value without a type assertion. + */ +export function getResultSchema(method: M): z.ZodType; +export function getResultSchema(method: string): z.ZodType | undefined; +export function getResultSchema(method: string): z.ZodType | undefined { + return hasResultMethod(method) ? resultSchemas[method] : undefined; +} + +/** + * Gets the Zod schema for a given request method. + * Returns `undefined` for non-spec methods. + * The typed overload returns a ZodType that parses to `RequestTypeMap[M]`, + * allowing callers to use `schema.parse()` without additional type assertions. + */ +export function getRequestSchema(method: M): z.ZodType; +export function getRequestSchema(method: string): z.ZodType | undefined; +export function getRequestSchema(method: string): z.ZodType | undefined { + return hasRequestMethod2025(method) ? requestSchemas[method] : undefined; +} + +/** + * Gets the Zod schema for a given notification method. + * Returns `undefined` for non-spec methods. + * @see getRequestSchema for the typed-overload contract. + */ +export function getNotificationSchema(method: M): z.ZodType; +export function getNotificationSchema(method: string): z.ZodType | undefined; +export function getNotificationSchema(method: string): z.ZodType | undefined { + return hasNotificationMethod2025(method) ? notificationSchemas[method] : undefined; +} + +/** Registry method lists (for the spec-method universe and the CI registry-diff oracle). */ +export const rev2025RequestMethods: readonly string[] = Object.keys(requestSchemas); +export const rev2025NotificationMethods: readonly string[] = Object.keys(notificationSchemas); diff --git a/packages/core/src/wire/rev2025-11-25/schemas.ts b/packages/core/src/wire/rev2025-11-25/schemas.ts new file mode 100644 index 0000000000..3c62d7f900 --- /dev/null +++ b/packages/core/src/wire/rev2025-11-25/schemas.ts @@ -0,0 +1,326 @@ +/** + * 2025-era wire schemas: the task family (protocol revision 2025-11-25) and + * the era's full wire role unions. + * + * Everything here is 2025-only WIRE vocabulary, physically absent from the + * neutral model layer and from the 2026-era codec (Q1 increment 2 - deletions + * are physical). The task message surface was restored types-only by #2248 + * for interop with task-capable 2025 peers and is parsed ONLY through this + * era's registry; the deprecated Task* TYPES remain importable from the types + * barrel (Q1-SD2: nameability is constant, runtime availability is + * version-keyed) but appear in no API signature. + * + * Shared-tier adjudications (documented deviations from a full relocation; + * each would otherwise change frozen 2025 parse behavior, Q10-L2): + * - `RelatedTaskMetadataSchema` stays in the neutral `RequestMetaSchema`: + * `io.modelcontextprotocol/related-task` is NORMATIVE 2025-11-25 `_meta` + * vocabulary, not a leak, and the wire-only lift deliberately exempts it. + * - `TaskMetadataSchema`/`TaskAugmentedRequestParamsSchema` stay neutral: + * they are the (deprecated) `task` param member composed into the shared + * request-param schemas; removing the declared key would change strip-mode + * parsing for 2025 peers. + * - The `tasks` capability sub-schemas stay on the shared capability + * schemas for the same reason; the 2026-era codec strips `capabilities.tasks` + * on encode instead (Q1-SD3 iii). + */ +import * as z from 'zod/v4'; + +import { + BaseRequestParamsSchema, + CallToolRequestSchema, + CallToolResultSchema, + CancelledNotificationSchema, + ClientNotificationSchema as NeutralClientNotificationSchema, + ClientRequestSchema as NeutralClientRequestSchema, + ClientResultSchema as NeutralClientResultSchema, + CompleteRequestSchema, + CompleteResultSchema, + CreateMessageRequestSchema, + CreateMessageResultSchema, + CreateMessageResultWithToolsSchema, + ElicitationCompleteNotificationSchema, + ElicitRequestSchema, + ElicitResultSchema, + EmptyResultSchema, + GetPromptRequestSchema, + GetPromptResultSchema, + InitializedNotificationSchema, + InitializeRequestSchema, + InitializeResultSchema, + ListPromptsRequestSchema, + ListPromptsResultSchema, + ListResourcesRequestSchema, + ListResourcesResultSchema, + ListResourceTemplatesRequestSchema, + ListResourceTemplatesResultSchema, + ListRootsRequestSchema, + ListRootsResultSchema, + ListToolsRequestSchema, + ListToolsResultSchema, + LoggingMessageNotificationSchema, + NotificationSchema, + NotificationsParamsSchema, + PaginatedRequestSchema, + PaginatedResultSchema, + PingRequestSchema, + ProgressNotificationSchema, + PromptListChangedNotificationSchema, + ReadResourceRequestSchema, + ReadResourceResultSchema, + RequestSchema, + ResourceListChangedNotificationSchema, + ResourceUpdatedNotificationSchema, + ResultSchema, + RootsListChangedNotificationSchema, + SetLevelRequestSchema, + SubscribeRequestSchema, + ToolListChangedNotificationSchema, + UnsubscribeRequestSchema +} from '../../types/schemas.js'; + +/** + * Task creation parameters, used to ask that the server create a task to represent a request. + * + * @deprecated 2025-11-25 wire vocabulary with no SDK runtime; kept importable for interoperability only. + */ +export const TaskCreationParamsSchema = z.looseObject({ + /** + * Requested duration in milliseconds to retain task from creation. + */ + ttl: z.number().optional(), + + /** + * Time in milliseconds to wait between task status requests. + */ + pollInterval: z.number().optional() +}); + +/** + * The status of a task. + * + * @deprecated 2025-11-25 wire vocabulary with no SDK runtime; kept importable for interoperability only. + */ +export const TaskStatusSchema = z.enum(['working', 'input_required', 'completed', 'failed', 'cancelled']); + +/* Tasks */ +/** + * A pollable state object associated with a request. + * + * @deprecated 2025-11-25 wire vocabulary with no SDK runtime; kept importable for interoperability only. + */ +export const TaskSchema = z.object({ + taskId: z.string(), + status: TaskStatusSchema, + /** + * Time in milliseconds to keep task results available after completion. + * If `null`, the task has unlimited lifetime until manually cleaned up. + */ + ttl: z.union([z.number(), z.null()]), + /** + * ISO 8601 timestamp when the task was created. + */ + createdAt: z.string(), + /** + * ISO 8601 timestamp when the task was last updated. + */ + lastUpdatedAt: z.string(), + pollInterval: z.optional(z.number()), + /** + * Optional diagnostic message for failed tasks or other status information. + */ + statusMessage: z.optional(z.string()) +}); + +/** + * Result returned when a task is created, containing the task data wrapped in a `task` field. + * + * @deprecated 2025-11-25 wire vocabulary with no SDK runtime; kept importable for interoperability only. + */ +export const CreateTaskResultSchema = ResultSchema.extend({ + task: TaskSchema +}); + +/** + * Parameters for task status notification. + * + * @deprecated 2025-11-25 wire vocabulary with no SDK runtime; kept importable for interoperability only. + */ +export const TaskStatusNotificationParamsSchema = NotificationsParamsSchema.merge(TaskSchema); + +/** + * A notification sent when a task's status changes. + * + * @deprecated 2025-11-25 wire vocabulary with no SDK runtime; kept importable for interoperability only. + */ +export const TaskStatusNotificationSchema = NotificationSchema.extend({ + method: z.literal('notifications/tasks/status'), + params: TaskStatusNotificationParamsSchema +}); + +/** + * A request to get the state of a specific task. + * + * @deprecated 2025-11-25 wire vocabulary with no SDK runtime; kept importable for interoperability only. + */ +export const GetTaskRequestSchema = RequestSchema.extend({ + method: z.literal('tasks/get'), + params: BaseRequestParamsSchema.extend({ + taskId: z.string() + }) +}); + +/** + * The response to a {@linkcode GetTaskRequest | tasks/get} request. + * + * @deprecated 2025-11-25 wire vocabulary with no SDK runtime; kept importable for interoperability only. + */ +export const GetTaskResultSchema = ResultSchema.merge(TaskSchema); + +/** + * A request to get the result of a specific task. + * + * @deprecated 2025-11-25 wire vocabulary with no SDK runtime; kept importable for interoperability only. + */ +export const GetTaskPayloadRequestSchema = RequestSchema.extend({ + method: z.literal('tasks/result'), + params: BaseRequestParamsSchema.extend({ + taskId: z.string() + }) +}); + +/** + * The response to a `tasks/result` request. + * The structure matches the result type of the original request. + * For example, a {@linkcode CallToolRequest | tools/call} task would return the `CallToolResult` structure. + * + * + * @deprecated 2025-11-25 wire vocabulary with no SDK runtime; kept importable for interoperability only. + */ +export const GetTaskPayloadResultSchema = ResultSchema.loose(); + +/** + * A request to list tasks. + * + * @deprecated 2025-11-25 wire vocabulary with no SDK runtime; kept importable for interoperability only. + */ +export const ListTasksRequestSchema = PaginatedRequestSchema.extend({ + method: z.literal('tasks/list') +}); + +/** + * The response to a {@linkcode ListTasksRequest | tasks/list} request. + * + * @deprecated 2025-11-25 wire vocabulary with no SDK runtime; kept importable for interoperability only. + */ +export const ListTasksResultSchema = PaginatedResultSchema.extend({ + tasks: z.array(TaskSchema) +}); + +/** + * A request to cancel a specific task. + * + * @deprecated 2025-11-25 wire vocabulary with no SDK runtime; kept importable for interoperability only. + */ +export const CancelTaskRequestSchema = RequestSchema.extend({ + method: z.literal('tasks/cancel'), + params: BaseRequestParamsSchema.extend({ + taskId: z.string() + }) +}); + +/** + * The response to a {@linkcode CancelTaskRequest | tasks/cancel} request. + * + * @deprecated 2025-11-25 wire vocabulary with no SDK runtime; kept importable for interoperability only. + */ +export const CancelTaskResultSchema = ResultSchema.merge(TaskSchema); + +/* The 2025-era wire role unions: the neutral message sets PLUS the task + * vocabulary. These are the era-faithful aggregates (what a 2025-11-25 peer + * may legally put on the wire, per role) and the source the era registry is + * built from. Member order preserves the pre-split unions (task members + * last for requests/results; notification members are method-discriminated, + * so ordering is not observable). */ +export const ClientRequestSchema = z.union([ + PingRequestSchema, + InitializeRequestSchema, + CompleteRequestSchema, + SetLevelRequestSchema, + GetPromptRequestSchema, + ListPromptsRequestSchema, + ListResourcesRequestSchema, + ListResourceTemplatesRequestSchema, + ReadResourceRequestSchema, + SubscribeRequestSchema, + UnsubscribeRequestSchema, + CallToolRequestSchema, + ListToolsRequestSchema, + GetTaskRequestSchema, + GetTaskPayloadRequestSchema, + ListTasksRequestSchema, + CancelTaskRequestSchema +]); + +export const ClientNotificationSchema = z.union([ + CancelledNotificationSchema, + ProgressNotificationSchema, + InitializedNotificationSchema, + RootsListChangedNotificationSchema, + TaskStatusNotificationSchema +]); + +export const ClientResultSchema = z.union([ + EmptyResultSchema, + CreateMessageResultSchema, + CreateMessageResultWithToolsSchema, + ElicitResultSchema, + ListRootsResultSchema, + GetTaskResultSchema, + ListTasksResultSchema, + CreateTaskResultSchema +]); + +export const ServerRequestSchema = z.union([ + PingRequestSchema, + CreateMessageRequestSchema, + ElicitRequestSchema, + ListRootsRequestSchema, + GetTaskRequestSchema, + GetTaskPayloadRequestSchema, + ListTasksRequestSchema, + CancelTaskRequestSchema +]); + +export const ServerNotificationSchema = z.union([ + CancelledNotificationSchema, + ProgressNotificationSchema, + LoggingMessageNotificationSchema, + ResourceUpdatedNotificationSchema, + ResourceListChangedNotificationSchema, + ToolListChangedNotificationSchema, + PromptListChangedNotificationSchema, + TaskStatusNotificationSchema, + ElicitationCompleteNotificationSchema +]); + +export const ServerResultSchema = z.union([ + EmptyResultSchema, + InitializeResultSchema, + CompleteResultSchema, + GetPromptResultSchema, + ListPromptsResultSchema, + ListResourcesResultSchema, + ListResourceTemplatesResultSchema, + ReadResourceResultSchema, + CallToolResultSchema, + ListToolsResultSchema, + GetTaskResultSchema, + ListTasksResultSchema, + CreateTaskResultSchema +]); + +// Reference the imported neutral aggregates so the relationship is explicit +// for readers and tooling: the wire unions above are strict supersets. +void NeutralClientRequestSchema; +void NeutralClientNotificationSchema; +void NeutralClientResultSchema; diff --git a/packages/core/src/wire/rev2025-11-25/wireTypes.ts b/packages/core/src/wire/rev2025-11-25/wireTypes.ts new file mode 100644 index 0000000000..f1c116ccad --- /dev/null +++ b/packages/core/src/wire/rev2025-11-25/wireTypes.ts @@ -0,0 +1,163 @@ +/** + * 2025-era WIRE-VIEW types: the anchor-exact 2025-11-25 shapes for the names + * whose NEUTRAL public types deliberately follow the 2026-07-28 typing. + * + * This module is the visible home of the shared-tier ADJUDICATIONS that the + * old `@ts-expect-error` affordances used to suppress (Q1 increment 2): each + * override below names a field where the 2025 anchor and the neutral model + * disagree, states which side the neutral model follows, and is pinned both + * ways by the per-revision parity suite (spec.types.2025-11-25.test.ts + * compares THESE types against the frozen anchor exactly — zero affordances). + * + * RUNTIME NOTE (Q10-L2): the 2025-era runtime schemas are BEHAVIOR-FROZEN + * and deliberately stay tolerant-wider than these wire views where the + * neutral typing is wider (e.g. `experimental` values accept any JSONObject + * at parse). These types pin the WIRE-LEVEL shape contract against the + * anchor; they do not narrow runtime acceptance. + * + * Adjudication ledger (neutral follows 2026 unless stated): + * - `Tool.inputSchema`/`outputSchema` property values: 2025 wire `object`; + * neutral follows 2026 (`JSONValue`-capable open schema objects). + * - capability blobs (`experimental`, `sampling`, `elicitation`, `tasks`, + * `logging`, `completions`): 2025 wire `object`; neutral `JSONObject`. + * - `extensions` capability key: 2026-only; absent from the 2025 wire view. + * - `CreateMessageRequestParams.metadata`: 2025 wire `object`; neutral + * `JSONObject`. + * - `PromptArgument.title` / `PromptReference.title`: present on the 2025 + * wire (BaseMetadata); the neutral schemas do not declare it and the + * strip-mode parse drops it (PRE-EXISTING runtime gap, recorded in the + * project baseline-bug log — do not silently change parse behavior here). + */ +import type { + CallToolRequest, + CancelTaskRequest, + ClientCapabilities, + CompleteRequest, + CreateMessageRequest, + CreateMessageRequestParams, + ElicitRequest, + GetPromptRequest, + GetTaskPayloadRequest, + GetTaskRequest, + InitializeRequest, + InitializeRequestParams, + InitializeResult, + ListPromptsRequest, + ListResourcesRequest, + ListResourceTemplatesRequest, + ListRootsRequest, + ListTasksRequest, + ListToolsRequest, + ListToolsResult, + PingRequest, + PromptArgument, + PromptReference, + ReadResourceRequest, + ServerCapabilities, + SetLevelRequest, + SubscribeRequest, + Tool, + UnsubscribeRequest +} from '../../types/types.js'; + +/** The 2025 anchor types blob values as bare `object`. */ +type ObjectMap = { [key: string]: object }; + +/** + * Omit that survives loose (index-signature) source types: the plain `Omit` + * collapses named keys into the index signature (`Pick`), which + * silently weakens the pins. Key-remapping preserves both. + */ +type OmitKnown = { [P in keyof T as P extends K ? never : P]: T[P] }; + +/** 2025 wire shape of tool input/output schemas (property values are `object`). */ +export type Wire2025ToolIOSchema = { + $schema?: string; + type: 'object'; + properties?: ObjectMap; + required?: string[]; +}; + +export type Wire2025Tool = OmitKnown & { + inputSchema: Wire2025ToolIOSchema; + outputSchema?: Wire2025ToolIOSchema; +}; + +export type Wire2025ListToolsResult = OmitKnown & { tools: Wire2025Tool[] }; + +export type Wire2025ClientCapabilities = OmitKnown< + ClientCapabilities, + 'extensions' | 'experimental' | 'sampling' | 'elicitation' | 'tasks' +> & { + experimental?: ObjectMap; + sampling?: { context?: object; tools?: object }; + elicitation?: { form?: object; url?: object }; + tasks?: { + list?: object; + cancel?: object; + requests?: { sampling?: { createMessage?: object }; elicitation?: { create?: object } }; + }; +}; + +export type Wire2025ServerCapabilities = OmitKnown< + ServerCapabilities, + 'extensions' | 'experimental' | 'logging' | 'completions' | 'tasks' +> & { + experimental?: ObjectMap; + logging?: object; + completions?: object; + tasks?: { + list?: object; + cancel?: object; + requests?: { tools?: { call?: object } }; + }; +}; + +export type Wire2025InitializeRequestParams = OmitKnown & { + capabilities: Wire2025ClientCapabilities; +}; + +export type Wire2025InitializeRequest = OmitKnown & { params: Wire2025InitializeRequestParams }; + +export type Wire2025InitializeResult = OmitKnown & { capabilities: Wire2025ServerCapabilities }; + +export type Wire2025CreateMessageRequestParams = OmitKnown & { + metadata?: object; + tools?: Wire2025Tool[]; +}; + +export type Wire2025CreateMessageRequest = OmitKnown & { params: Wire2025CreateMessageRequestParams }; + +/** 2025 wire: `title` is a declared BaseMetadata member (the neutral schemas do not model it — see ledger above). */ +export type Wire2025PromptArgument = PromptArgument & { title?: string }; +export type Wire2025PromptReference = PromptReference & { title?: string }; + +/** The 2025 wire role unions with the adjudicated members substituted. */ +export type Wire2025ClientRequestView = + | PingRequest + | Wire2025InitializeRequest + | CompleteRequest + | SetLevelRequest + | GetPromptRequest + | ListPromptsRequest + | ListResourcesRequest + | ListResourceTemplatesRequest + | ReadResourceRequest + | SubscribeRequest + | UnsubscribeRequest + | CallToolRequest + | ListToolsRequest + | GetTaskRequest + | GetTaskPayloadRequest + | ListTasksRequest + | CancelTaskRequest; + +export type Wire2025ServerRequestView = + | PingRequest + | Wire2025CreateMessageRequest + | ElicitRequest + | ListRootsRequest + | GetTaskRequest + | GetTaskPayloadRequest + | ListTasksRequest + | CancelTaskRequest; diff --git a/packages/core/src/wire/rev2026-07-28/codec.ts b/packages/core/src/wire/rev2026-07-28/codec.ts new file mode 100644 index 0000000000..9e3e4f25ef --- /dev/null +++ b/packages/core/src/wire/rev2026-07-28/codec.ts @@ -0,0 +1,207 @@ +/** + * The 2026-era wire codec (protocol revision 2026-07-28). + * + * Decode = raw-first `resultType` discrimination (the structural V-1 home: + * the RAW value is inspected BEFORE any schema validation, so a non-complete + * result can never be masked into a hollow success by a tolerant schema), + * then wire-exact parse, then lift (drop the wire member). Encode = the + * stamp seam: `resultType: 'complete'` is stamped on outbound results, and + * the known deleted-field set is strictly enforced (Q1-SD3 iii) — the 2026 + * wire types have no slot for `execution.taskSupport` or + * `capabilities.tasks`, so the encode mapping deletes them; era-blind + * handlers stay era-invisible while deleted vocabulary cannot cross eras + * through the parse-free outbound path. + * + * Q1-SD3 postures implemented here: + * (i) absent `resultType` from a 2026-classified peer → typed error NAMING + * the violation. The spec's absent⇒complete bridge is scoped to + * EARLIER-revision servers (spec.types.2026-07-28.ts Result.resultType: + * "Servers implementing this protocol version MUST include this field") + * and is deliberately NOT extended to modern traffic. + * (ii) `input_required` → the driver-seam payload (the multi-round-trip + * driver, M4.1/#13, consumes it; until then the protocol layer surfaces + * the discriminated kind as a typed local error, no retry). + * (iii) unrecognized kinds → invalid, no retry (DQ5). + * + * The ttlMs/cacheScope stamping content (M3.2) lands in `encodeResult` — + * this seam is its final home. + */ +import type * as z from 'zod/v4'; + +import { SdkError, SdkErrorCode } from '../../errors/sdkErrors.js'; +import type { Result } from '../../types/types.js'; +import type { DecodedResult, LiftedWireMaterial, WireCodec } from '../codec.js'; +import { + getNotificationSchema2026, + getRequestSchema2026, + getResultSchema2026, + hasNotificationMethod2026, + hasRequestMethod2026 +} from './registry.js'; +import { + CallToolResultSchema, + CompleteResultSchema, + DiscoverResultSchema, + GetPromptResultSchema, + ListPromptsResultSchema, + ListResourcesResultSchema, + ListResourceTemplatesResultSchema, + ListToolsResultSchema, + ReadResourceResultSchema, + RequestMetaEnvelopeSchema +} from './schemas.js'; + +function isPlainObject(value: unknown): value is Record { + return value !== null && typeof value === 'object' && !Array.isArray(value); +} + +/** Strip the known deleted-field set from an outbound result (Q1-SD3 iii). */ +function enforceDeletedFields(method: string, result: Result): Result { + let next: Record = result as Record; + let copied = false; + const copy = () => { + if (!copied) { + next = { ...next }; + copied = true; + } + return next; + }; + + // tools arrays: execution (the taskSupport carrier) is deleted vocabulary. + const tools = (result as { tools?: unknown }).tools; + if (method === 'tools/list' && Array.isArray(tools) && tools.some(tool => isPlainObject(tool) && 'execution' in tool)) { + copy().tools = tools.map(tool => { + if (!isPlainObject(tool) || !('execution' in tool)) return tool; + const rest = { ...tool }; + delete rest['execution']; + return rest; + }); + } + + // capability objects: the `tasks` capability is deleted vocabulary. + const capabilities = (result as { capabilities?: unknown }).capabilities; + if (isPlainObject(capabilities) && 'tasks' in capabilities) { + const rest = { ...capabilities }; + delete rest['tasks']; + copy().capabilities = rest; + } + + return next as Result; +} + +export const rev2026Codec: WireCodec = { + era: '2026-07-28', + + hasRequestMethod: hasRequestMethod2026, + hasNotificationMethod: hasNotificationMethod2026, + + requestSchema: getRequestSchema2026, + resultSchema: getResultSchema2026, + notificationSchema: getNotificationSchema2026, + + decodeResult(method: string, raw: unknown): DecodedResult { + if (!isPlainObject(raw)) { + return { + kind: 'invalid', + error: new SdkError(SdkErrorCode.InvalidResult, `Invalid result for ${method}: not an object`, { method }) + }; + } + + // Step 1 — RAW discrimination, before any schema (V-1). + const rawResultType = raw['resultType']; + if (rawResultType === undefined) { + // Q1-SD3 (i): hard error naming the violation. + return { + kind: 'invalid', + error: new SdkError( + SdkErrorCode.InvalidResult, + `Invalid result for ${method}: missing required resultType — servers implementing protocol revision 2026-07-28 ` + + `MUST include it (the absent-means-complete bridge applies only to earlier-revision servers)`, + { method, violation: 'missing-resultType' } + ) + }; + } + if (typeof rawResultType !== 'string') { + return { + kind: 'invalid', + error: new SdkError(SdkErrorCode.InvalidResult, `Invalid result for ${method}: non-string resultType`, { + method, + resultType: rawResultType + }) + }; + } + if (rawResultType === 'input_required') { + // The driver seam (#13 consumes this payload). + const inputRequests = raw['inputRequests']; + return { + kind: 'input_required', + inputRequests: isPlainObject(inputRequests) ? inputRequests : {}, + ...(typeof raw['requestState'] === 'string' && { requestState: raw['requestState'] }) + }; + } + if (rawResultType !== 'complete') { + // Unrecognized kind ⇒ invalid, no retry (DQ5). + return { + kind: 'invalid', + error: new SdkError(SdkErrorCode.UnsupportedResultType, `Unsupported result type '${rawResultType}' for ${method}`, { + resultType: rawResultType, + method + }) + }; + } + + // Step 2 — wire-exact parse (registry methods), with resultType present. + // Own-key lookup: `method` is peer-influenced on related-request + // paths, and a prototype-chain hit (e.g. 'constructor') must not + // masquerade as a schema and throw out of the decode hop. + const wireSchema = Object.hasOwn(WIRE_RESULT_SCHEMAS, method) ? WIRE_RESULT_SCHEMAS[method] : undefined; + if (wireSchema !== undefined) { + const parsed = wireSchema.safeParse(raw); + if (!parsed.success) { + return { + kind: 'invalid', + error: new SdkError(SdkErrorCode.InvalidResult, `Invalid result for ${method}: ${parsed.error}`, { method }) + }; + } + } + + // Step 3 — lift: the wire discriminator is consumed. + const lifted = { ...raw }; + delete lifted['resultType']; + return { kind: 'complete', result: lifted as Result }; + }, + + encodeResult(method: string, result: Result): Result { + // The stamp seam: outbound results carry the required discriminator. + // (Handler-authored resultType for methods whose vocabulary exceeds + // 'complete' is MRTR scope — #13 extends this seam.) + return { ...enforceDeletedFields(method, result), resultType: 'complete' } as Result; + }, + + checkInboundEnvelope(material: LiftedWireMaterial): string | undefined { + if (material.envelope === undefined) { + return ( + 'Request is missing the required _meta envelope for protocol revision 2026-07-28 ' + + '(io.modelcontextprotocol/protocolVersion, io.modelcontextprotocol/clientInfo, io.modelcontextprotocol/clientCapabilities)' + ); + } + const parsed = RequestMetaEnvelopeSchema.safeParse(material.envelope); + if (!parsed.success) { + return `Invalid _meta envelope for protocol revision 2026-07-28: ${parsed.error.issues.map(issue => issue.message).join('; ')}`; + } + return undefined; + } +}; + +/** Wire-true result wrappers consulted by decode step 2, keyed by method. */ +const WIRE_RESULT_SCHEMAS: Record = { + 'tools/call': CallToolResultSchema, + 'tools/list': ListToolsResultSchema, + 'prompts/get': GetPromptResultSchema, + 'prompts/list': ListPromptsResultSchema, + 'resources/list': ListResourcesResultSchema, + 'resources/templates/list': ListResourceTemplatesResultSchema, + 'resources/read': ReadResourceResultSchema, + 'completion/complete': CompleteResultSchema, + 'server/discover': DiscoverResultSchema +}; diff --git a/packages/core/src/wire/rev2026-07-28/registry.ts b/packages/core/src/wire/rev2026-07-28/registry.ts new file mode 100644 index 0000000000..e361e65eff --- /dev/null +++ b/packages/core/src/wire/rev2026-07-28/registry.ts @@ -0,0 +1,84 @@ +/** + * The 2026-era method registries (protocol revision 2026-07-28). + * + * Registry membership IS the deletion story: there are NO entries for + * `initialize`, `notifications/initialized`, `ping`, `logging/setLevel`, + * `resources/subscribe`, `resources/unsubscribe`, + * `notifications/roots/list_changed`, the task family, or the server→client + * wire-request channel — so an era-mismatched method falls to −32601 by + * absence inbound and a typed local error outbound, with no table to forget. + * + * HAND-REGISTRY SEED DECISIONS (pinned by the CI registry-diff oracle, which + * fails LOUD if this list and the anchor diff ever disagree): + * - `sampling/createMessage`, `elicitation/create`, `roots/list`: the anchor + * still carries their method literals on bare interfaces, but 2026 DEMOTES + * them from wire requests to in-band `InputRequest` payloads — the entire + * server→client JSON-RPC request channel is deleted (`ServerRequest` has + * no 2026 export). A generator walking method literals would re-admit them + * (the ATK-D flavor-b trap); this hand registry excludes them by + * construction. Their in-band role lands with the MRTR driver (#13). + * - `subscriptions/listen` + `notifications/subscriptions/acknowledged` + * (SEP-1865): 2026-only vocabulary whose SHELLS land with the + * subscriptions feature (#14). Until then they are absent here — inbound + * listen gets −32601 (capability not yet served), which is protocol-legal + * for a server that does not implement subscriptions. + */ +import type * as z from 'zod/v4'; + +import type { NotificationMethod, NotificationTypeMap, RequestMethod, RequestTypeMap, ResultTypeMap } from '../../types/types.js'; +import type { Rev2026NotificationMethod, Rev2026RequestMethod } from './schemas.js'; +import { dispatchRequestSchemas, dispatchResultSchemas, notificationSchemas2026 } from './schemas.js'; + +/** The 2026-era request-method set (registry membership = the deletion story). */ +export function hasRequestMethod2026(method: string): method is Rev2026RequestMethod { + return Object.prototype.hasOwnProperty.call(dispatchRequestSchemas, method); +} + +/** The 2026-era notification-method set. */ +export function hasNotificationMethod2026(method: string): method is Rev2026NotificationMethod { + return Object.prototype.hasOwnProperty.call(notificationSchemas2026, method); +} + +/** Result-map membership (same key set as the request map on this era). */ +function hasResultMethod2026(method: string): method is Rev2026RequestMethod { + return Object.prototype.hasOwnProperty.call(dispatchResultSchemas, method); +} + +/** + * Gets the dispatch (post-lift) Zod schema for a given request method. + * Returns `undefined` for methods this era's registry does not define. + * The typed overload mirrors `WireCodec.requestSchema` so call sites with a + * statically known method need no type assertion. + */ +export function getRequestSchema2026(method: M): z.ZodType | undefined; +export function getRequestSchema2026(method: string): z.ZodType | undefined; +export function getRequestSchema2026(method: string): z.ZodType | undefined { + return hasRequestMethod2026(method) ? dispatchRequestSchemas[method] : undefined; +} + +/** + * Gets the dispatch (post-lift) Zod schema for validating results of a given + * request method. Returns `undefined` for methods this era's registry does + * not define. + * @see getRequestSchema2026 for the typed-overload contract. + */ +export function getResultSchema2026(method: M): z.ZodType | undefined; +export function getResultSchema2026(method: string): z.ZodType | undefined; +export function getResultSchema2026(method: string): z.ZodType | undefined { + return hasResultMethod2026(method) ? dispatchResultSchemas[method] : undefined; +} + +/** + * Gets the Zod schema for a given notification method. + * Returns `undefined` for methods this era's registry does not define. + * @see getRequestSchema2026 for the typed-overload contract. + */ +export function getNotificationSchema2026(method: M): z.ZodType | undefined; +export function getNotificationSchema2026(method: string): z.ZodType | undefined; +export function getNotificationSchema2026(method: string): z.ZodType | undefined { + return hasNotificationMethod2026(method) ? notificationSchemas2026[method] : undefined; +} + +/** Registry method lists (for the spec-method universe and the CI registry-diff oracle). */ +export const rev2026RequestMethods: readonly string[] = Object.keys(dispatchRequestSchemas); +export const rev2026NotificationMethods: readonly string[] = Object.keys(notificationSchemas2026); diff --git a/packages/core/src/wire/rev2026-07-28/schemas.ts b/packages/core/src/wire/rev2026-07-28/schemas.ts new file mode 100644 index 0000000000..510cd399ef --- /dev/null +++ b/packages/core/src/wire/rev2026-07-28/schemas.ts @@ -0,0 +1,490 @@ +/** + * 2026-era wire schemas (protocol revision 2026-07-28). + * + * This module is the only place the per-request `_meta` envelope is modeled. + * The envelope is wire-only vocabulary: the protocol layer lifts it off + * inbound requests before any handler runs and surfaces it at + * `ctx.mcpReq.envelope`; the 2026-era codec enforces its requiredness at + * dispatch time (`checkInboundEnvelope`) - the former neutral-schema JSDoc + * deferral ("enforced per request at dispatch time, not here") is now + * discharged by that codec step. + * + * No 2025-era traffic ever touches this module, so requiredness here is + * bare and spec-exact (the shared-schema `.catch` hazards do not apply). + */ +import * as z from 'zod/v4'; + +import { + CLIENT_CAPABILITIES_META_KEY, + CLIENT_INFO_META_KEY, + LOG_LEVEL_META_KEY, + PROTOCOL_VERSION_META_KEY +} from '../../types/constants.js'; +import { + AnnotationsSchema, + AudioContentSchema, + BaseMetadataSchema, + BlobResourceContentsSchema, + CancelledNotificationSchema, + ClientCapabilitiesSchema, + ContentBlockSchema, + CursorSchema, + ElicitationCompleteNotificationSchema, + IconsSchema, + ImageContentSchema, + ImplementationSchema, + LoggingLevelSchema, + LoggingMessageNotificationSchema, + ProgressNotificationSchema, + ProgressTokenSchema, + PromptListChangedNotificationSchema, + PromptMessageSchema, + PromptReferenceSchema, + PromptSchema, + ResourceContentsSchema, + ResourceListChangedNotificationSchema, + ResourceSchema, + ResourceTemplateReferenceSchema, + ResourceTemplateSchema, + ResourceUpdatedNotificationSchema, + RoleSchema, + ServerCapabilitiesSchema, + TextContentSchema, + TextResourceContentsSchema, + ToolAnnotationsSchema, + ToolListChangedNotificationSchema, + ToolUseContentSchema +} from '../../types/schemas.js'; + +/* 2026-era capability forks (defined ahead of the envelope, which composes + * the client fork). The shared shapes minus the deleted `tasks` key: `tasks` + * is 2025-only vocabulary with no slot on this revision, consistent with the + * encode-side deletion (Q1-SD3 iii). + * + * The client fork lists its members EXPLICITLY (composing the shared member + * schemas by reference) rather than using `.omit()`: the envelope schema + * below reaches the bundled package declarations, and an `.omit()` inference + * is a mapped type whose printed member order is unstable across dts-rollup + * builds (api-report flap). The explicit list doubles as the fork's deletion + * statement — a member added to the shared shape must be re-adjudicated here. */ +const sharedClientCapabilityShape = ClientCapabilitiesSchema.shape; +export const ClientCapabilities2026Schema = z.object({ + experimental: sharedClientCapabilityShape.experimental, + sampling: sharedClientCapabilityShape.sampling, + elicitation: sharedClientCapabilityShape.elicitation, + roots: sharedClientCapabilityShape.roots, + extensions: sharedClientCapabilityShape.extensions +}); +export const ServerCapabilities2026Schema = ServerCapabilitiesSchema.omit({ tasks: true }); + +/* Per-request `_meta` envelope */ +/** + * The per-request `_meta` envelope carried by every request under protocol revision + * 2026-07-28: the protocol version governing the request, the client implementation + * info, and the client's capabilities — declared per request rather than once at + * initialization — plus the optional log-level opt-in. + * + * This schema models the complete envelope on its own (loose: foreign keys + * pass through - the lift extracts exactly the reserved keys, so enforcement + * never sees extension material). Requiredness is enforced per request at + * dispatch time by the 2026-era codec's `checkInboundEnvelope` step. + */ +export const RequestMetaEnvelopeSchema = z.looseObject({ + /** + * If specified, the caller is requesting out-of-band progress notifications for this request (as represented by 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: ProgressTokenSchema.optional(), + /** + * The MCP protocol version being used for this request. For the HTTP transport, + * the value must match the `MCP-Protocol-Version` header. + */ + [PROTOCOL_VERSION_META_KEY]: z.string(), + /** + * Identifies the client software making the request. + */ + [CLIENT_INFO_META_KEY]: ImplementationSchema, + /** + * The client's capabilities for this specific request. An empty object means the + * client supports no optional capabilities. Servers must not infer capabilities + * from prior requests. Validated with the 2026 fork: `tasks` has no slot on + * this revision (deleted vocabulary), matching the server-side fork wired + * into `DiscoverResultSchema`. + */ + [CLIENT_CAPABILITIES_META_KEY]: ClientCapabilities2026Schema, + /** + * The desired log level for this request. When absent, the server must not send + * `notifications/message` notifications for the request. + * + * @deprecated Deprecated as of protocol version 2026-07-28 (SEP-2577); remains + * in the specification for at least twelve months. + */ + [LOG_LEVEL_META_KEY]: LoggingLevelSchema.optional() +}); + +/* ------------------------------------------------------------------------ * + * Forked payload vocabulary (shared-tier admission rule, ATK-B section 1): + * `Tool` and `SamplingMessage` are bidirectionally incomparable between the + * 2025-11-25 and 2026-07-28 anchors, so they FORK per wire module instead of + * sitting in the shared tier. The forks below are 2026-anchor-exact: + * - Tool (2026) has NO `execution` member (ToolExecution and its + * `taskSupport` carrier are deleted vocabulary) — a 2026 peer's tool that + * carries one is stripped on parse, and the encode side strips it from + * outbound tools (Q1-SD3 iii). + * - SamplingMessage (2026) is composed against the 2026 anchor shape. + * ------------------------------------------------------------------------ */ + +/** 2026-era Tool: anchor-exact — no `execution` (deleted vocabulary). */ +export const ToolSchema = z.object({ + ...BaseMetadataSchema.shape, + ...IconsSchema.shape, + description: z.string().optional(), + // Anchor-exact: { $schema?: string; type: 'object'; [key: string]: unknown } + inputSchema: z.looseObject({ + $schema: z.string().optional(), + type: z.literal('object') + }), + // Anchor-exact: { $schema?: string; [key: string]: unknown } + outputSchema: z + .looseObject({ + $schema: z.string().optional() + }) + .optional(), + annotations: ToolAnnotationsSchema.optional(), + _meta: z.record(z.string(), z.unknown()).optional() +}); + +/** 2026-era ToolResultContent (anchor-exact: `structuredContent?: unknown`). */ +export const ToolResultContentSchema = z.object({ + type: z.literal('tool_result'), + toolUseId: z.string(), + content: z.array(ContentBlockSchema), + structuredContent: z.unknown().optional(), + isError: z.boolean().optional(), + _meta: z.record(z.string(), z.unknown()).optional() +}); + +/** 2026-era sampling content union (composes the forked tool-result shape). */ +export const SamplingMessageContentBlockSchema = z.union([ + TextContentSchema, + ImageContentSchema, + AudioContentSchema, + ToolUseContentSchema, + ToolResultContentSchema +]); + +/** 2026-era SamplingMessage (anchor-exact: single block or array). */ +export const SamplingMessageSchema = z.object({ + role: RoleSchema, + content: z.union([SamplingMessageContentBlockSchema, z.array(SamplingMessageContentBlockSchema)]), + _meta: z.record(z.string(), z.unknown()).optional() +}); + +/* ------------------------------------------------------------------------ * + * Result side. `resultType` is REQUIRED at parse (spec.types.2026-07-28 + * Result.resultType: "Servers implementing this protocol version MUST + * include this field"); requiredness is bare because no 2025-era traffic + * touches this module. These are the WIRE-TRUE artifacts — the corpus and + * the parity suite parse them; `decodeResult` parses with them and then + * LIFTS (drops resultType) to the neutral shape. + * ------------------------------------------------------------------------ */ + +/** Open union per the anchor: 'complete' | 'input_required' | string. */ +export const ResultTypeSchema = z.string(); + +const wireMeta = z.record(z.string(), z.unknown()).optional(); + +function wireResult(shape: T) { + return z.looseObject({ + _meta: wireMeta, + /** REQUIRED on this revision (see module header). */ + resultType: ResultTypeSchema, + ...shape + }); +} + +export const ResultSchema = wireResult({}); + +export const PaginatedResultSchema = wireResult({ + nextCursor: CursorSchema.optional() +}); + +export const CallToolResultSchema = wireResult({ + content: z.array(ContentBlockSchema), + structuredContent: z.unknown().optional(), + isError: z.boolean().optional() +}); + +export const ListToolsResultSchema = wireResult({ + ttlMs: z.number().int().min(0), + cacheScope: z.enum(['public', 'private']), + tools: z.array(ToolSchema), + nextCursor: CursorSchema.optional() +}); + +export const ListPromptsResultSchema = wireResult({ + ttlMs: z.number().int().min(0), + cacheScope: z.enum(['public', 'private']), + prompts: z.array(PromptSchema), + nextCursor: CursorSchema.optional() +}); + +export const GetPromptResultSchema = wireResult({ + description: z.string().optional(), + messages: z.array(PromptMessageSchema) +}); + +export const ListResourcesResultSchema = wireResult({ + ttlMs: z.number().int().min(0), + cacheScope: z.enum(['public', 'private']), + resources: z.array(ResourceSchema), + nextCursor: CursorSchema.optional() +}); + +export const ListResourceTemplatesResultSchema = wireResult({ + ttlMs: z.number().int().min(0), + cacheScope: z.enum(['public', 'private']), + resourceTemplates: z.array(ResourceTemplateSchema), + nextCursor: CursorSchema.optional() +}); + +export const ReadResourceResultSchema = wireResult({ + ttlMs: z.number().int().min(0), + cacheScope: z.enum(['public', 'private']), + contents: z.array(z.union([TextResourceContentsSchema, BlobResourceContentsSchema])) +}); + +export const CompleteResultSchema = wireResult({ + completion: z + .object({ + values: z.array(z.string()).max(100), + total: z.number().int().optional(), + hasMore: z.boolean().optional() + }) + .loose() +}); + +/** CacheableResult (SEP-2549): ttlMs and cacheScope REQUIRED per the anchor. */ +export const CacheableResultSchema = wireResult({ + ttlMs: z.number().int().min(0), + cacheScope: z.enum(['public', 'private']) +}); + +export const DiscoverResultSchema = wireResult({ + ttlMs: z.number().int().min(0), + cacheScope: z.enum(['public', 'private']), + supportedVersions: z.array(z.string()), + capabilities: ServerCapabilities2026Schema, + serverInfo: ImplementationSchema, + instructions: z.string().optional() +}); + +/* ------------------------------------------------------------------------ * + * Request side. Two views per method: + * - WIRE-TRUE (`RequestSchema`): params `_meta` carries the REQUIRED + * envelope (anchor RequestParams._meta is required). The corpus and parity + * suite consume these. + * - DISPATCH (post-lift, internal to the registry): the protocol layer's + * universal lift has already extracted the envelope, so dispatch parses a + * 2025-like shape with optional `_meta` (progressToken/extension keys + * only) and NO 2025-only members (`task` is undeclared and strips — + * payload-level deletion is physical on this leg). + * ------------------------------------------------------------------------ */ + +/** Post-lift request `_meta` (progressToken + extension keys; loose). */ +const DispatchRequestMetaSchema = z.looseObject({ + progressToken: ProgressTokenSchema.optional() +}); + +function wireRequest(method: M, paramsShape: T) { + return z.object({ + method: z.literal(method), + params: z.object({ _meta: RequestMetaEnvelopeSchema, ...paramsShape }) + }); +} + +function dispatchRequest(method: M, paramsShape: T) { + return z.object({ + method: z.literal(method), + params: z.object({ _meta: DispatchRequestMetaSchema.optional(), ...paramsShape }).optional() + }); +} + +const callToolParamsShape = { + name: z.string(), + arguments: z.record(z.string(), z.unknown()).optional() +}; +const paginatedParamsShape = { cursor: CursorSchema.optional() }; + +export const CallToolRequestSchema = wireRequest('tools/call', callToolParamsShape); +export const ListToolsRequestSchema = wireRequest('tools/list', paginatedParamsShape); +export const ListPromptsRequestSchema = wireRequest('prompts/list', paginatedParamsShape); +export const GetPromptRequestSchema = wireRequest('prompts/get', { + name: z.string(), + arguments: z.record(z.string(), z.string()).optional() +}); +export const ListResourcesRequestSchema = wireRequest('resources/list', paginatedParamsShape); +export const ListResourceTemplatesRequestSchema = wireRequest('resources/templates/list', paginatedParamsShape); +export const ReadResourceRequestSchema = wireRequest('resources/read', { uri: z.string() }); +const completeParamsShape = { + ref: z.union([PromptReferenceSchema, ResourceTemplateReferenceSchema]), + argument: z.object({ name: z.string(), value: z.string() }), + context: z.object({ arguments: z.record(z.string(), z.string()).optional() }).optional() +}; +export const CompleteRequestSchema = wireRequest('completion/complete', completeParamsShape); +export const DiscoverRequestSchema = wireRequest('server/discover', {}); + +/** + * The 2026-era request-method set — the hand-registry seed (see registry.ts + * for the seed decisions). The dispatch maps below are mapped types over this + * union, so a missing entry, an extra entry, or an entry pointing at another + * method's schema is a compile error; the CI registry-diff oracle pins the + * same set against the anchor at runtime. + */ +export type Rev2026RequestMethod = + | 'tools/call' + | 'tools/list' + | 'prompts/get' + | 'prompts/list' + | 'resources/list' + | 'resources/templates/list' + | 'resources/read' + | 'completion/complete' + | 'server/discover'; + +/** Dispatch (post-lift) request schemas, keyed by method — registry-internal. */ +export const dispatchRequestSchemas: { readonly [M in Rev2026RequestMethod]: z.ZodType<{ method: M }> } = { + 'tools/call': dispatchRequest('tools/call', callToolParamsShape), + 'tools/list': dispatchRequest('tools/list', paginatedParamsShape), + 'prompts/get': dispatchRequest('prompts/get', { + name: z.string(), + arguments: z.record(z.string(), z.string()).optional() + }), + 'prompts/list': dispatchRequest('prompts/list', paginatedParamsShape), + 'resources/list': dispatchRequest('resources/list', paginatedParamsShape), + 'resources/templates/list': dispatchRequest('resources/templates/list', paginatedParamsShape), + 'resources/read': dispatchRequest('resources/read', { uri: z.string() }), + 'completion/complete': dispatchRequest('completion/complete', completeParamsShape), + 'server/discover': dispatchRequest('server/discover', {}) +}; + +/** Dispatch (post-lift) result schemas, keyed by method — what the funnel + * validates AFTER `decodeResult` consumed `resultType`. */ +function liftedResult(shape: T) { + return z.looseObject({ _meta: wireMeta, ...shape }); +} + +export const dispatchResultSchemas: { readonly [M in Rev2026RequestMethod]: z.ZodType } = { + 'tools/call': liftedResult({ + content: z.array(ContentBlockSchema), + structuredContent: z.unknown().optional(), + isError: z.boolean().optional() + }), + 'tools/list': liftedResult({ + ttlMs: z.number().int().min(0), + cacheScope: z.enum(['public', 'private']), + tools: z.array(ToolSchema), + nextCursor: CursorSchema.optional() + }), + 'prompts/get': liftedResult({ + description: z.string().optional(), + messages: z.array(PromptMessageSchema) + }), + 'prompts/list': liftedResult({ + ttlMs: z.number().int().min(0), + cacheScope: z.enum(['public', 'private']), + prompts: z.array(PromptSchema), + nextCursor: CursorSchema.optional() + }), + 'resources/list': liftedResult({ + ttlMs: z.number().int().min(0), + cacheScope: z.enum(['public', 'private']), + resources: z.array(ResourceSchema), + nextCursor: CursorSchema.optional() + }), + 'resources/templates/list': liftedResult({ + ttlMs: z.number().int().min(0), + cacheScope: z.enum(['public', 'private']), + resourceTemplates: z.array(ResourceTemplateSchema), + nextCursor: CursorSchema.optional() + }), + 'resources/read': liftedResult({ + ttlMs: z.number().int().min(0), + cacheScope: z.enum(['public', 'private']), + contents: z.array(z.union([TextResourceContentsSchema, BlobResourceContentsSchema])) + }), + 'completion/complete': liftedResult({ + completion: z + .object({ + values: z.array(z.string()).max(100), + total: z.number().int().optional(), + hasMore: z.boolean().optional() + }) + .loose() + }), + 'server/discover': liftedResult({ + ttlMs: z.number().int().min(0), + cacheScope: z.enum(['public', 'private']), + supportedVersions: z.array(z.string()), + capabilities: ServerCapabilities2026Schema, + serverInfo: ImplementationSchema, + instructions: z.string().optional() + }) +}; + +/* ------------------------------------------------------------------------ * + * Notifications. The 2026 notification set: cancelled, progress, message, + * resources/updated, resources/list_changed, tools/list_changed, + * prompts/list_changed, elicitation/complete. Deleted: initialized, + * roots/list_changed, tasks/status. The shapes are revision-identical to the + * shared schemas, which are composed by reference. (The 2026-only + * subscriptions/acknowledged notification is #14 scope — see registry.ts.) + * ------------------------------------------------------------------------ */ +/** The 2026-era notification-method set (the hand-registry seed; see the deletion list above). */ +export type Rev2026NotificationMethod = + | 'notifications/cancelled' + | 'notifications/progress' + | 'notifications/message' + | 'notifications/resources/updated' + | 'notifications/resources/list_changed' + | 'notifications/tools/list_changed' + | 'notifications/prompts/list_changed' + | 'notifications/elicitation/complete'; + +export const notificationSchemas2026: { readonly [M in Rev2026NotificationMethod]: z.ZodType<{ method: M }> } = { + 'notifications/cancelled': CancelledNotificationSchema, + 'notifications/progress': ProgressNotificationSchema, + 'notifications/message': LoggingMessageNotificationSchema, + 'notifications/resources/updated': ResourceUpdatedNotificationSchema, + 'notifications/resources/list_changed': ResourceListChangedNotificationSchema, + 'notifications/tools/list_changed': ToolListChangedNotificationSchema, + 'notifications/prompts/list_changed': PromptListChangedNotificationSchema, + 'notifications/elicitation/complete': ElicitationCompleteNotificationSchema +}; + +/* ------------------------------------------------------------------------ * + * Response envelopes (wire-true; parity/corpus artifacts). + * ------------------------------------------------------------------------ */ +const wireResultResponse = (result: T) => + z + .object({ + jsonrpc: z.literal('2.0'), + id: z.union([z.string(), z.number().int()]), + result + }) + .strict(); + +export const JSONRPCResultResponseSchema = wireResultResponse(ResultSchema); +export const CallToolResultResponseSchema = wireResultResponse(CallToolResultSchema); +export const ListToolsResultResponseSchema = wireResultResponse(ListToolsResultSchema); +export const ListPromptsResultResponseSchema = wireResultResponse(ListPromptsResultSchema); +export const GetPromptResultResponseSchema = wireResultResponse(GetPromptResultSchema); +export const ListResourcesResultResponseSchema = wireResultResponse(ListResourcesResultSchema); +export const ListResourceTemplatesResultResponseSchema = wireResultResponse(ListResourceTemplatesResultSchema); +export const ReadResourceResultResponseSchema = wireResultResponse(ReadResourceResultSchema); +export const CompleteResultResponseSchema = wireResultResponse(CompleteResultSchema); +export const DiscoverResultResponseSchema = wireResultResponse(DiscoverResultSchema); + +// Referenced by reference to keep the compose-by-reference relationships +// explicit for tooling (these shared payloads serve both eras unchanged). +void AnnotationsSchema; +void ResourceContentsSchema; diff --git a/packages/core/test/corpus/fixtures/2025-11-25/CallToolRequest/call-tool-with-progress-token.json b/packages/core/test/corpus/fixtures/2025-11-25/CallToolRequest/call-tool-with-progress-token.json new file mode 100644 index 0000000000..a19422351c --- /dev/null +++ b/packages/core/test/corpus/fixtures/2025-11-25/CallToolRequest/call-tool-with-progress-token.json @@ -0,0 +1,12 @@ +{ + "method": "tools/call", + "params": { + "name": "get_weather", + "arguments": { + "location": "Berlin" + }, + "_meta": { + "progressToken": 7 + } + } +} diff --git a/packages/core/test/corpus/fixtures/2025-11-25/CallToolRequest/call-tool.json b/packages/core/test/corpus/fixtures/2025-11-25/CallToolRequest/call-tool.json new file mode 100644 index 0000000000..a4a986baae --- /dev/null +++ b/packages/core/test/corpus/fixtures/2025-11-25/CallToolRequest/call-tool.json @@ -0,0 +1,9 @@ +{ + "method": "tools/call", + "params": { + "name": "get_weather", + "arguments": { + "location": "New York" + } + } +} diff --git a/packages/core/test/corpus/fixtures/2025-11-25/CallToolResult/is-error.json b/packages/core/test/corpus/fixtures/2025-11-25/CallToolResult/is-error.json new file mode 100644 index 0000000000..6d1b416593 --- /dev/null +++ b/packages/core/test/corpus/fixtures/2025-11-25/CallToolResult/is-error.json @@ -0,0 +1,9 @@ +{ + "content": [ + { + "type": "text", + "text": "Failed to fetch weather data: API rate limit exceeded" + } + ], + "isError": true +} diff --git a/packages/core/test/corpus/fixtures/2025-11-25/CallToolResult/structured.json b/packages/core/test/corpus/fixtures/2025-11-25/CallToolResult/structured.json new file mode 100644 index 0000000000..6c88b928ab --- /dev/null +++ b/packages/core/test/corpus/fixtures/2025-11-25/CallToolResult/structured.json @@ -0,0 +1,12 @@ +{ + "content": [ + { + "type": "text", + "text": "{\"temperature\": 22.5}" + } + ], + "structuredContent": { + "temperature": 22.5, + "conditions": "Partly cloudy" + } +} diff --git a/packages/core/test/corpus/fixtures/2025-11-25/CallToolResult/text.json b/packages/core/test/corpus/fixtures/2025-11-25/CallToolResult/text.json new file mode 100644 index 0000000000..1675638535 --- /dev/null +++ b/packages/core/test/corpus/fixtures/2025-11-25/CallToolResult/text.json @@ -0,0 +1,8 @@ +{ + "content": [ + { + "type": "text", + "text": "Current weather in New York: 72F, partly cloudy" + } + ] +} diff --git a/packages/core/test/corpus/fixtures/2025-11-25/CancelledNotification/cancelled.json b/packages/core/test/corpus/fixtures/2025-11-25/CancelledNotification/cancelled.json new file mode 100644 index 0000000000..ec61e4267b --- /dev/null +++ b/packages/core/test/corpus/fixtures/2025-11-25/CancelledNotification/cancelled.json @@ -0,0 +1,7 @@ +{ + "method": "notifications/cancelled", + "params": { + "requestId": 12, + "reason": "User requested cancellation" + } +} diff --git a/packages/core/test/corpus/fixtures/2025-11-25/CompleteRequest/complete.json b/packages/core/test/corpus/fixtures/2025-11-25/CompleteRequest/complete.json new file mode 100644 index 0000000000..161168c398 --- /dev/null +++ b/packages/core/test/corpus/fixtures/2025-11-25/CompleteRequest/complete.json @@ -0,0 +1,13 @@ +{ + "method": "completion/complete", + "params": { + "ref": { + "type": "ref/prompt", + "name": "code_review" + }, + "argument": { + "name": "language", + "value": "py" + } + } +} diff --git a/packages/core/test/corpus/fixtures/2025-11-25/CompleteResult/complete-result.json b/packages/core/test/corpus/fixtures/2025-11-25/CompleteResult/complete-result.json new file mode 100644 index 0000000000..99b8c5a8d7 --- /dev/null +++ b/packages/core/test/corpus/fixtures/2025-11-25/CompleteResult/complete-result.json @@ -0,0 +1,7 @@ +{ + "completion": { + "values": ["python", "pytorch", "pyside"], + "total": 10, + "hasMore": true + } +} diff --git a/packages/core/test/corpus/fixtures/2025-11-25/CreateMessageRequest/create-message.json b/packages/core/test/corpus/fixtures/2025-11-25/CreateMessageRequest/create-message.json new file mode 100644 index 0000000000..2376b12004 --- /dev/null +++ b/packages/core/test/corpus/fixtures/2025-11-25/CreateMessageRequest/create-message.json @@ -0,0 +1,25 @@ +{ + "method": "sampling/createMessage", + "params": { + "messages": [ + { + "role": "user", + "content": { + "type": "text", + "text": "What is the capital of France?" + } + } + ], + "modelPreferences": { + "hints": [ + { + "name": "claude-3-sonnet" + } + ], + "intelligencePriority": 0.8, + "speedPriority": 0.5 + }, + "systemPrompt": "You are a helpful assistant.", + "maxTokens": 100 + } +} diff --git a/packages/core/test/corpus/fixtures/2025-11-25/CreateMessageResult/create-message-result.json b/packages/core/test/corpus/fixtures/2025-11-25/CreateMessageResult/create-message-result.json new file mode 100644 index 0000000000..74d3e63b6a --- /dev/null +++ b/packages/core/test/corpus/fixtures/2025-11-25/CreateMessageResult/create-message-result.json @@ -0,0 +1,9 @@ +{ + "role": "assistant", + "content": { + "type": "text", + "text": "The capital of France is Paris." + }, + "model": "claude-3-sonnet-20240307", + "stopReason": "endTurn" +} diff --git a/packages/core/test/corpus/fixtures/2025-11-25/CreateTaskResult/create-task.json b/packages/core/test/corpus/fixtures/2025-11-25/CreateTaskResult/create-task.json new file mode 100644 index 0000000000..1cbdac652e --- /dev/null +++ b/packages/core/test/corpus/fixtures/2025-11-25/CreateTaskResult/create-task.json @@ -0,0 +1,10 @@ +{ + "task": { + "taskId": "786af6b0-2779-48ed-9cc1-b8a8a25b8a86", + "status": "working", + "createdAt": "2025-11-25T10:30:00Z", + "ttl": 60000, + "pollInterval": 5000, + "lastUpdatedAt": "2025-11-25T10:30:05Z" + } +} diff --git a/packages/core/test/corpus/fixtures/2025-11-25/ElicitRequest/form.json b/packages/core/test/corpus/fixtures/2025-11-25/ElicitRequest/form.json new file mode 100644 index 0000000000..b7c223f106 --- /dev/null +++ b/packages/core/test/corpus/fixtures/2025-11-25/ElicitRequest/form.json @@ -0,0 +1,16 @@ +{ + "method": "elicitation/create", + "params": { + "mode": "form", + "message": "Please provide your GitHub username", + "requestedSchema": { + "type": "object", + "properties": { + "name": { + "type": "string" + } + }, + "required": ["name"] + } + } +} diff --git a/packages/core/test/corpus/fixtures/2025-11-25/ElicitResult/accept.json b/packages/core/test/corpus/fixtures/2025-11-25/ElicitResult/accept.json new file mode 100644 index 0000000000..9b9b00f3a4 --- /dev/null +++ b/packages/core/test/corpus/fixtures/2025-11-25/ElicitResult/accept.json @@ -0,0 +1,6 @@ +{ + "action": "accept", + "content": { + "name": "octocat" + } +} diff --git a/packages/core/test/corpus/fixtures/2025-11-25/EmptyResult/empty.json b/packages/core/test/corpus/fixtures/2025-11-25/EmptyResult/empty.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/packages/core/test/corpus/fixtures/2025-11-25/EmptyResult/empty.json @@ -0,0 +1 @@ +{} diff --git a/packages/core/test/corpus/fixtures/2025-11-25/GetPromptRequest/get-prompt.json b/packages/core/test/corpus/fixtures/2025-11-25/GetPromptRequest/get-prompt.json new file mode 100644 index 0000000000..10aef03748 --- /dev/null +++ b/packages/core/test/corpus/fixtures/2025-11-25/GetPromptRequest/get-prompt.json @@ -0,0 +1,9 @@ +{ + "method": "prompts/get", + "params": { + "name": "code_review", + "arguments": { + "code": "def hello():\n print('world')" + } + } +} diff --git a/packages/core/test/corpus/fixtures/2025-11-25/GetPromptResult/get-prompt-result.json b/packages/core/test/corpus/fixtures/2025-11-25/GetPromptResult/get-prompt-result.json new file mode 100644 index 0000000000..fcff6dfbcc --- /dev/null +++ b/packages/core/test/corpus/fixtures/2025-11-25/GetPromptResult/get-prompt-result.json @@ -0,0 +1,12 @@ +{ + "description": "Code review prompt", + "messages": [ + { + "role": "user", + "content": { + "type": "text", + "text": "Please review this code:\ndef hello():\n print('world')" + } + } + ] +} diff --git a/packages/core/test/corpus/fixtures/2025-11-25/GetTaskRequest/get-task.json b/packages/core/test/corpus/fixtures/2025-11-25/GetTaskRequest/get-task.json new file mode 100644 index 0000000000..b4bad8297a --- /dev/null +++ b/packages/core/test/corpus/fixtures/2025-11-25/GetTaskRequest/get-task.json @@ -0,0 +1,6 @@ +{ + "method": "tasks/get", + "params": { + "taskId": "786af6b0-2779-48ed-9cc1-b8a8a25b8a86" + } +} diff --git a/packages/core/test/corpus/fixtures/2025-11-25/InitializeRequest/initialize.json b/packages/core/test/corpus/fixtures/2025-11-25/InitializeRequest/initialize.json new file mode 100644 index 0000000000..e4a4ce60e1 --- /dev/null +++ b/packages/core/test/corpus/fixtures/2025-11-25/InitializeRequest/initialize.json @@ -0,0 +1,20 @@ +{ + "method": "initialize", + "params": { + "protocolVersion": "2025-11-25", + "capabilities": { + "roots": { + "listChanged": true + }, + "sampling": {}, + "elicitation": { + "form": {} + } + }, + "clientInfo": { + "name": "example-client", + "title": "Example Client", + "version": "1.0.0" + } + } +} diff --git a/packages/core/test/corpus/fixtures/2025-11-25/InitializeResult/initialize-result.json b/packages/core/test/corpus/fixtures/2025-11-25/InitializeResult/initialize-result.json new file mode 100644 index 0000000000..61db694725 --- /dev/null +++ b/packages/core/test/corpus/fixtures/2025-11-25/InitializeResult/initialize-result.json @@ -0,0 +1,22 @@ +{ + "protocolVersion": "2025-11-25", + "capabilities": { + "logging": {}, + "prompts": { + "listChanged": true + }, + "resources": { + "subscribe": true, + "listChanged": true + }, + "tools": { + "listChanged": true + } + }, + "serverInfo": { + "name": "example-server", + "title": "Example Server", + "version": "1.0.0" + }, + "instructions": "Optional instructions for the client." +} diff --git a/packages/core/test/corpus/fixtures/2025-11-25/InitializedNotification/initialized.json b/packages/core/test/corpus/fixtures/2025-11-25/InitializedNotification/initialized.json new file mode 100644 index 0000000000..de0aae9156 --- /dev/null +++ b/packages/core/test/corpus/fixtures/2025-11-25/InitializedNotification/initialized.json @@ -0,0 +1,3 @@ +{ + "method": "notifications/initialized" +} diff --git a/packages/core/test/corpus/fixtures/2025-11-25/JSONRPCErrorResponse/error-envelope.json b/packages/core/test/corpus/fixtures/2025-11-25/JSONRPCErrorResponse/error-envelope.json new file mode 100644 index 0000000000..75b928f98b --- /dev/null +++ b/packages/core/test/corpus/fixtures/2025-11-25/JSONRPCErrorResponse/error-envelope.json @@ -0,0 +1,8 @@ +{ + "jsonrpc": "2.0", + "id": 1, + "error": { + "code": -32602, + "message": "Unknown tool: invalid_tool_name" + } +} diff --git a/packages/core/test/corpus/fixtures/2025-11-25/JSONRPCRequest/request-envelope.json b/packages/core/test/corpus/fixtures/2025-11-25/JSONRPCRequest/request-envelope.json new file mode 100644 index 0000000000..3c9b8d5943 --- /dev/null +++ b/packages/core/test/corpus/fixtures/2025-11-25/JSONRPCRequest/request-envelope.json @@ -0,0 +1,11 @@ +{ + "jsonrpc": "2.0", + "id": 1, + "method": "tools/call", + "params": { + "name": "get_weather", + "arguments": { + "location": "New York" + } + } +} diff --git a/packages/core/test/corpus/fixtures/2025-11-25/JSONRPCResultResponse/result-envelope.json b/packages/core/test/corpus/fixtures/2025-11-25/JSONRPCResultResponse/result-envelope.json new file mode 100644 index 0000000000..09c1f92fee --- /dev/null +++ b/packages/core/test/corpus/fixtures/2025-11-25/JSONRPCResultResponse/result-envelope.json @@ -0,0 +1,12 @@ +{ + "jsonrpc": "2.0", + "id": 1, + "result": { + "content": [ + { + "type": "text", + "text": "72F, partly cloudy" + } + ] + } +} diff --git a/packages/core/test/corpus/fixtures/2025-11-25/ListPromptsResult/list-prompts-result.json b/packages/core/test/corpus/fixtures/2025-11-25/ListPromptsResult/list-prompts-result.json new file mode 100644 index 0000000000..478b405ada --- /dev/null +++ b/packages/core/test/corpus/fixtures/2025-11-25/ListPromptsResult/list-prompts-result.json @@ -0,0 +1,16 @@ +{ + "prompts": [ + { + "name": "code_review", + "title": "Request Code Review", + "description": "Asks the LLM to analyze code quality", + "arguments": [ + { + "name": "code", + "description": "The code to review", + "required": true + } + ] + } + ] +} diff --git a/packages/core/test/corpus/fixtures/2025-11-25/ListResourceTemplatesResult/list-templates-result.json b/packages/core/test/corpus/fixtures/2025-11-25/ListResourceTemplatesResult/list-templates-result.json new file mode 100644 index 0000000000..6798afa00e --- /dev/null +++ b/packages/core/test/corpus/fixtures/2025-11-25/ListResourceTemplatesResult/list-templates-result.json @@ -0,0 +1,11 @@ +{ + "resourceTemplates": [ + { + "uriTemplate": "file:///{path}", + "name": "Project Files", + "title": "Project Files", + "description": "Access files in the project directory", + "mimeType": "application/octet-stream" + } + ] +} diff --git a/packages/core/test/corpus/fixtures/2025-11-25/ListResourcesRequest/list-resources.json b/packages/core/test/corpus/fixtures/2025-11-25/ListResourcesRequest/list-resources.json new file mode 100644 index 0000000000..1114099b54 --- /dev/null +++ b/packages/core/test/corpus/fixtures/2025-11-25/ListResourcesRequest/list-resources.json @@ -0,0 +1,4 @@ +{ + "method": "resources/list", + "params": {} +} diff --git a/packages/core/test/corpus/fixtures/2025-11-25/ListResourcesResult/list-resources-result.json b/packages/core/test/corpus/fixtures/2025-11-25/ListResourcesResult/list-resources-result.json new file mode 100644 index 0000000000..96f8354bf5 --- /dev/null +++ b/packages/core/test/corpus/fixtures/2025-11-25/ListResourcesResult/list-resources-result.json @@ -0,0 +1,11 @@ +{ + "resources": [ + { + "uri": "file:///project/src/main.rs", + "name": "main.rs", + "title": "Rust Software Application Main File", + "description": "Primary application entry point", + "mimeType": "text/x-rust" + } + ] +} diff --git a/packages/core/test/corpus/fixtures/2025-11-25/ListRootsRequest/list-roots.json b/packages/core/test/corpus/fixtures/2025-11-25/ListRootsRequest/list-roots.json new file mode 100644 index 0000000000..5237f0ba98 --- /dev/null +++ b/packages/core/test/corpus/fixtures/2025-11-25/ListRootsRequest/list-roots.json @@ -0,0 +1,3 @@ +{ + "method": "roots/list" +} diff --git a/packages/core/test/corpus/fixtures/2025-11-25/ListRootsResult/list-roots-result.json b/packages/core/test/corpus/fixtures/2025-11-25/ListRootsResult/list-roots-result.json new file mode 100644 index 0000000000..1fdaed5db4 --- /dev/null +++ b/packages/core/test/corpus/fixtures/2025-11-25/ListRootsResult/list-roots-result.json @@ -0,0 +1,8 @@ +{ + "roots": [ + { + "uri": "file:///home/user/projects/myproject", + "name": "My Project" + } + ] +} diff --git a/packages/core/test/corpus/fixtures/2025-11-25/ListToolsRequest/list-tools.json b/packages/core/test/corpus/fixtures/2025-11-25/ListToolsRequest/list-tools.json new file mode 100644 index 0000000000..2c264f8727 --- /dev/null +++ b/packages/core/test/corpus/fixtures/2025-11-25/ListToolsRequest/list-tools.json @@ -0,0 +1,6 @@ +{ + "method": "tools/list", + "params": { + "cursor": "eyJwYWdlIjogM30=" + } +} diff --git a/packages/core/test/corpus/fixtures/2025-11-25/ListToolsResult/list-tools-result.json b/packages/core/test/corpus/fixtures/2025-11-25/ListToolsResult/list-tools-result.json new file mode 100644 index 0000000000..cc0eca1eff --- /dev/null +++ b/packages/core/test/corpus/fixtures/2025-11-25/ListToolsResult/list-tools-result.json @@ -0,0 +1,19 @@ +{ + "tools": [ + { + "name": "get_weather", + "title": "Weather Provider", + "description": "Get current weather for a location", + "inputSchema": { + "type": "object", + "properties": { + "location": { + "type": "string" + } + }, + "required": ["location"] + } + } + ], + "nextCursor": "eyJwYWdlIjogNH0=" +} diff --git a/packages/core/test/corpus/fixtures/2025-11-25/LoggingMessageNotification/log-message.json b/packages/core/test/corpus/fixtures/2025-11-25/LoggingMessageNotification/log-message.json new file mode 100644 index 0000000000..258aa12575 --- /dev/null +++ b/packages/core/test/corpus/fixtures/2025-11-25/LoggingMessageNotification/log-message.json @@ -0,0 +1,11 @@ +{ + "method": "notifications/message", + "params": { + "level": "error", + "logger": "database", + "data": { + "error": "Connection failed", + "host": "localhost" + } + } +} diff --git a/packages/core/test/corpus/fixtures/2025-11-25/PingRequest/ping.json b/packages/core/test/corpus/fixtures/2025-11-25/PingRequest/ping.json new file mode 100644 index 0000000000..9484af42e3 --- /dev/null +++ b/packages/core/test/corpus/fixtures/2025-11-25/PingRequest/ping.json @@ -0,0 +1,3 @@ +{ + "method": "ping" +} diff --git a/packages/core/test/corpus/fixtures/2025-11-25/ProgressNotification/progress.json b/packages/core/test/corpus/fixtures/2025-11-25/ProgressNotification/progress.json new file mode 100644 index 0000000000..5c78f7c64f --- /dev/null +++ b/packages/core/test/corpus/fixtures/2025-11-25/ProgressNotification/progress.json @@ -0,0 +1,9 @@ +{ + "method": "notifications/progress", + "params": { + "progressToken": 12, + "progress": 50, + "total": 100, + "message": "Halfway there" + } +} diff --git a/packages/core/test/corpus/fixtures/2025-11-25/PromptListChangedNotification/prompt-list-changed.json b/packages/core/test/corpus/fixtures/2025-11-25/PromptListChangedNotification/prompt-list-changed.json new file mode 100644 index 0000000000..ba487a2d5a --- /dev/null +++ b/packages/core/test/corpus/fixtures/2025-11-25/PromptListChangedNotification/prompt-list-changed.json @@ -0,0 +1,3 @@ +{ + "method": "notifications/prompts/list_changed" +} diff --git a/packages/core/test/corpus/fixtures/2025-11-25/ReadResourceRequest/read-resource.json b/packages/core/test/corpus/fixtures/2025-11-25/ReadResourceRequest/read-resource.json new file mode 100644 index 0000000000..fcebffa3d1 --- /dev/null +++ b/packages/core/test/corpus/fixtures/2025-11-25/ReadResourceRequest/read-resource.json @@ -0,0 +1,6 @@ +{ + "method": "resources/read", + "params": { + "uri": "file:///project/src/main.rs" + } +} diff --git a/packages/core/test/corpus/fixtures/2025-11-25/ReadResourceResult/blob.json b/packages/core/test/corpus/fixtures/2025-11-25/ReadResourceResult/blob.json new file mode 100644 index 0000000000..527388bde2 --- /dev/null +++ b/packages/core/test/corpus/fixtures/2025-11-25/ReadResourceResult/blob.json @@ -0,0 +1,9 @@ +{ + "contents": [ + { + "uri": "file:///project/assets/logo.png", + "mimeType": "image/png", + "blob": "iVBORw0KGgoAAAANSUhEUg==" + } + ] +} diff --git a/packages/core/test/corpus/fixtures/2025-11-25/ReadResourceResult/text.json b/packages/core/test/corpus/fixtures/2025-11-25/ReadResourceResult/text.json new file mode 100644 index 0000000000..1396a6bc0a --- /dev/null +++ b/packages/core/test/corpus/fixtures/2025-11-25/ReadResourceResult/text.json @@ -0,0 +1,9 @@ +{ + "contents": [ + { + "uri": "file:///project/src/main.rs", + "mimeType": "text/x-rust", + "text": "fn main() {\n println(\"Hello world\");\n}" + } + ] +} diff --git a/packages/core/test/corpus/fixtures/2025-11-25/ResourceListChangedNotification/resource-list-changed.json b/packages/core/test/corpus/fixtures/2025-11-25/ResourceListChangedNotification/resource-list-changed.json new file mode 100644 index 0000000000..5bec1a4c79 --- /dev/null +++ b/packages/core/test/corpus/fixtures/2025-11-25/ResourceListChangedNotification/resource-list-changed.json @@ -0,0 +1,3 @@ +{ + "method": "notifications/resources/list_changed" +} diff --git a/packages/core/test/corpus/fixtures/2025-11-25/ResourceUpdatedNotification/resource-updated.json b/packages/core/test/corpus/fixtures/2025-11-25/ResourceUpdatedNotification/resource-updated.json new file mode 100644 index 0000000000..9f942d4314 --- /dev/null +++ b/packages/core/test/corpus/fixtures/2025-11-25/ResourceUpdatedNotification/resource-updated.json @@ -0,0 +1,6 @@ +{ + "method": "notifications/resources/updated", + "params": { + "uri": "file:///project/src/main.rs" + } +} diff --git a/packages/core/test/corpus/fixtures/2025-11-25/RootsListChangedNotification/roots-list-changed.json b/packages/core/test/corpus/fixtures/2025-11-25/RootsListChangedNotification/roots-list-changed.json new file mode 100644 index 0000000000..dd94884afb --- /dev/null +++ b/packages/core/test/corpus/fixtures/2025-11-25/RootsListChangedNotification/roots-list-changed.json @@ -0,0 +1,3 @@ +{ + "method": "notifications/roots/list_changed" +} diff --git a/packages/core/test/corpus/fixtures/2025-11-25/SetLevelRequest/set-level.json b/packages/core/test/corpus/fixtures/2025-11-25/SetLevelRequest/set-level.json new file mode 100644 index 0000000000..849853b545 --- /dev/null +++ b/packages/core/test/corpus/fixtures/2025-11-25/SetLevelRequest/set-level.json @@ -0,0 +1,6 @@ +{ + "method": "logging/setLevel", + "params": { + "level": "info" + } +} diff --git a/packages/core/test/corpus/fixtures/2025-11-25/SubscribeRequest/subscribe.json b/packages/core/test/corpus/fixtures/2025-11-25/SubscribeRequest/subscribe.json new file mode 100644 index 0000000000..b478078154 --- /dev/null +++ b/packages/core/test/corpus/fixtures/2025-11-25/SubscribeRequest/subscribe.json @@ -0,0 +1,6 @@ +{ + "method": "resources/subscribe", + "params": { + "uri": "file:///project/src/main.rs" + } +} diff --git a/packages/core/test/corpus/fixtures/2025-11-25/TaskAugmentedRequestParams/task-augmented-call-params.json b/packages/core/test/corpus/fixtures/2025-11-25/TaskAugmentedRequestParams/task-augmented-call-params.json new file mode 100644 index 0000000000..881f113ff9 --- /dev/null +++ b/packages/core/test/corpus/fixtures/2025-11-25/TaskAugmentedRequestParams/task-augmented-call-params.json @@ -0,0 +1,10 @@ +{ + "task": { + "ttl": 60000 + }, + "_meta": { + "io.modelcontextprotocol/related-task": { + "taskId": "786af6b0-2779-48ed-9cc1-b8a8a25b8a86" + } + } +} diff --git a/packages/core/test/corpus/fixtures/2025-11-25/TaskStatusNotification/task-status.json b/packages/core/test/corpus/fixtures/2025-11-25/TaskStatusNotification/task-status.json new file mode 100644 index 0000000000..170b49bebe --- /dev/null +++ b/packages/core/test/corpus/fixtures/2025-11-25/TaskStatusNotification/task-status.json @@ -0,0 +1,11 @@ +{ + "method": "notifications/tasks/status", + "params": { + "taskId": "786af6b0-2779-48ed-9cc1-b8a8a25b8a86", + "status": "working", + "statusMessage": "Processing input", + "createdAt": "2025-11-25T10:30:00Z", + "ttl": 60000, + "lastUpdatedAt": "2025-11-25T10:30:05Z" + } +} diff --git a/packages/core/test/corpus/fixtures/2025-11-25/ToolListChangedNotification/tool-list-changed.json b/packages/core/test/corpus/fixtures/2025-11-25/ToolListChangedNotification/tool-list-changed.json new file mode 100644 index 0000000000..c9c29c4e10 --- /dev/null +++ b/packages/core/test/corpus/fixtures/2025-11-25/ToolListChangedNotification/tool-list-changed.json @@ -0,0 +1,3 @@ +{ + "method": "notifications/tools/list_changed" +} diff --git a/packages/core/test/corpus/fixtures/2025-11-25/UnsubscribeRequest/unsubscribe.json b/packages/core/test/corpus/fixtures/2025-11-25/UnsubscribeRequest/unsubscribe.json new file mode 100644 index 0000000000..ce9b642f8e --- /dev/null +++ b/packages/core/test/corpus/fixtures/2025-11-25/UnsubscribeRequest/unsubscribe.json @@ -0,0 +1,6 @@ +{ + "method": "resources/unsubscribe", + "params": { + "uri": "file:///project/src/main.rs" + } +} diff --git a/packages/core/test/corpus/fixtures/2026-07-28/AudioContent/audio-wav-content.json b/packages/core/test/corpus/fixtures/2026-07-28/AudioContent/audio-wav-content.json new file mode 100644 index 0000000000..1816ec4416 --- /dev/null +++ b/packages/core/test/corpus/fixtures/2026-07-28/AudioContent/audio-wav-content.json @@ -0,0 +1,5 @@ +{ + "type": "audio", + "data": "UklGRiQAAABXQVZFZm10IBAAAAABAAEARKwAAIhYAQACABAAZGF0YQAAAAA=", + "mimeType": "audio/wav" +} diff --git a/packages/core/test/corpus/fixtures/2026-07-28/BlobResourceContents/image-file-contents.json b/packages/core/test/corpus/fixtures/2026-07-28/BlobResourceContents/image-file-contents.json new file mode 100644 index 0000000000..5b9ef07c9c --- /dev/null +++ b/packages/core/test/corpus/fixtures/2026-07-28/BlobResourceContents/image-file-contents.json @@ -0,0 +1,5 @@ +{ + "uri": "file:///example.png", + "mimeType": "image/png", + "blob": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==" +} diff --git a/packages/core/test/corpus/fixtures/2026-07-28/BooleanSchema/boolean-input-schema.json b/packages/core/test/corpus/fixtures/2026-07-28/BooleanSchema/boolean-input-schema.json new file mode 100644 index 0000000000..48d6d589c1 --- /dev/null +++ b/packages/core/test/corpus/fixtures/2026-07-28/BooleanSchema/boolean-input-schema.json @@ -0,0 +1,6 @@ +{ + "type": "boolean", + "title": "Display Name", + "description": "Description text", + "default": false +} diff --git a/packages/core/test/corpus/fixtures/2026-07-28/CallToolRequest/call-tool-request.json b/packages/core/test/corpus/fixtures/2026-07-28/CallToolRequest/call-tool-request.json new file mode 100644 index 0000000000..2429aeca86 --- /dev/null +++ b/packages/core/test/corpus/fixtures/2026-07-28/CallToolRequest/call-tool-request.json @@ -0,0 +1,19 @@ +{ + "jsonrpc": "2.0", + "id": "call-tool-example", + "method": "tools/call", + "params": { + "_meta": { + "io.modelcontextprotocol/protocolVersion": "2026-07-28", + "io.modelcontextprotocol/clientInfo": { + "name": "ExampleClient", + "version": "1.0.0" + }, + "io.modelcontextprotocol/clientCapabilities": {} + }, + "name": "get_weather", + "arguments": { + "location": "New York" + } + } +} diff --git a/packages/core/test/corpus/fixtures/2026-07-28/CallToolRequestParams/get-weather-tool-call-params.json b/packages/core/test/corpus/fixtures/2026-07-28/CallToolRequestParams/get-weather-tool-call-params.json new file mode 100644 index 0000000000..c65f9ceae8 --- /dev/null +++ b/packages/core/test/corpus/fixtures/2026-07-28/CallToolRequestParams/get-weather-tool-call-params.json @@ -0,0 +1,14 @@ +{ + "_meta": { + "io.modelcontextprotocol/protocolVersion": "2026-07-28", + "io.modelcontextprotocol/clientInfo": { + "name": "ExampleClient", + "version": "1.0.0" + }, + "io.modelcontextprotocol/clientCapabilities": {} + }, + "name": "get_weather", + "arguments": { + "location": "New York" + } +} diff --git a/packages/core/test/corpus/fixtures/2026-07-28/CallToolRequestParams/tool-call-params-with-progress-token.json b/packages/core/test/corpus/fixtures/2026-07-28/CallToolRequestParams/tool-call-params-with-progress-token.json new file mode 100644 index 0000000000..8335d11a4e --- /dev/null +++ b/packages/core/test/corpus/fixtures/2026-07-28/CallToolRequestParams/tool-call-params-with-progress-token.json @@ -0,0 +1,15 @@ +{ + "_meta": { + "io.modelcontextprotocol/protocolVersion": "2026-07-28", + "io.modelcontextprotocol/clientInfo": { + "name": "ExampleClient", + "version": "1.0.0" + }, + "io.modelcontextprotocol/clientCapabilities": {}, + "progressToken": "oivaizmir" + }, + "name": "build_simulation", + "arguments": { + "city": "Micropolis" + } +} diff --git a/packages/core/test/corpus/fixtures/2026-07-28/CallToolResult/invalid-tool-input-error.json b/packages/core/test/corpus/fixtures/2026-07-28/CallToolResult/invalid-tool-input-error.json new file mode 100644 index 0000000000..59648895c2 --- /dev/null +++ b/packages/core/test/corpus/fixtures/2026-07-28/CallToolResult/invalid-tool-input-error.json @@ -0,0 +1,10 @@ +{ + "resultType": "complete", + "content": [ + { + "type": "text", + "text": "Invalid departure date: must be in the future. Current date is 08/08/2025." + } + ], + "isError": true +} diff --git a/packages/core/test/corpus/fixtures/2026-07-28/CallToolResult/result-with-array-structured-content.json b/packages/core/test/corpus/fixtures/2026-07-28/CallToolResult/result-with-array-structured-content.json new file mode 100644 index 0000000000..ccb136143b --- /dev/null +++ b/packages/core/test/corpus/fixtures/2026-07-28/CallToolResult/result-with-array-structured-content.json @@ -0,0 +1,13 @@ +{ + "resultType": "complete", + "content": [ + { + "type": "text", + "text": "Found 2 users: Alice (alice@example.com) and Bob (bob@example.com)." + } + ], + "structuredContent": [ + { "id": "1", "name": "Alice", "email": "alice@example.com" }, + { "id": "2", "name": "Bob", "email": "bob@example.com" } + ] +} diff --git a/packages/core/test/corpus/fixtures/2026-07-28/CallToolResult/result-with-structured-content.json b/packages/core/test/corpus/fixtures/2026-07-28/CallToolResult/result-with-structured-content.json new file mode 100644 index 0000000000..b7a9cdb80f --- /dev/null +++ b/packages/core/test/corpus/fixtures/2026-07-28/CallToolResult/result-with-structured-content.json @@ -0,0 +1,14 @@ +{ + "resultType": "complete", + "content": [ + { + "type": "text", + "text": "{\"temperature\": 22.5, \"conditions\": \"Partly cloudy\", \"humidity\": 65}" + } + ], + "structuredContent": { + "temperature": 22.5, + "conditions": "Partly cloudy", + "humidity": 65 + } +} diff --git a/packages/core/test/corpus/fixtures/2026-07-28/CallToolResult/result-with-unstructured-text.json b/packages/core/test/corpus/fixtures/2026-07-28/CallToolResult/result-with-unstructured-text.json new file mode 100644 index 0000000000..4f54c48d0a --- /dev/null +++ b/packages/core/test/corpus/fixtures/2026-07-28/CallToolResult/result-with-unstructured-text.json @@ -0,0 +1,10 @@ +{ + "resultType": "complete", + "content": [ + { + "type": "text", + "text": "Current weather in New York:\nTemperature: 72°F\nConditions: Partly cloudy" + } + ], + "isError": false +} diff --git a/packages/core/test/corpus/fixtures/2026-07-28/CallToolResultResponse/call-tool-result-response.json b/packages/core/test/corpus/fixtures/2026-07-28/CallToolResultResponse/call-tool-result-response.json new file mode 100644 index 0000000000..da4c062ca5 --- /dev/null +++ b/packages/core/test/corpus/fixtures/2026-07-28/CallToolResultResponse/call-tool-result-response.json @@ -0,0 +1,14 @@ +{ + "jsonrpc": "2.0", + "id": "call-tool-example", + "result": { + "resultType": "complete", + "content": [ + { + "type": "text", + "text": "Current weather in New York:\nTemperature: 72°F\nConditions: Partly cloudy" + } + ], + "isError": false + } +} diff --git a/packages/core/test/corpus/fixtures/2026-07-28/CancelledNotification/user-requested-cancellation.json b/packages/core/test/corpus/fixtures/2026-07-28/CancelledNotification/user-requested-cancellation.json new file mode 100644 index 0000000000..aa52f8e4c5 --- /dev/null +++ b/packages/core/test/corpus/fixtures/2026-07-28/CancelledNotification/user-requested-cancellation.json @@ -0,0 +1,8 @@ +{ + "jsonrpc": "2.0", + "method": "notifications/cancelled", + "params": { + "requestId": "123", + "reason": "User requested cancellation" + } +} diff --git a/packages/core/test/corpus/fixtures/2026-07-28/CancelledNotificationParams/user-requested-cancellation.json b/packages/core/test/corpus/fixtures/2026-07-28/CancelledNotificationParams/user-requested-cancellation.json new file mode 100644 index 0000000000..fb032ac1b4 --- /dev/null +++ b/packages/core/test/corpus/fixtures/2026-07-28/CancelledNotificationParams/user-requested-cancellation.json @@ -0,0 +1,4 @@ +{ + "requestId": "123", + "reason": "User requested cancellation" +} diff --git a/packages/core/test/corpus/fixtures/2026-07-28/ClientCapabilities/elicitation-form-and-url-mode-support.json b/packages/core/test/corpus/fixtures/2026-07-28/ClientCapabilities/elicitation-form-and-url-mode-support.json new file mode 100644 index 0000000000..ca75391d3b --- /dev/null +++ b/packages/core/test/corpus/fixtures/2026-07-28/ClientCapabilities/elicitation-form-and-url-mode-support.json @@ -0,0 +1,6 @@ +{ + "elicitation": { + "form": {}, + "url": {} + } +} diff --git a/packages/core/test/corpus/fixtures/2026-07-28/ClientCapabilities/elicitation-form-only-implicit.json b/packages/core/test/corpus/fixtures/2026-07-28/ClientCapabilities/elicitation-form-only-implicit.json new file mode 100644 index 0000000000..29786c4c03 --- /dev/null +++ b/packages/core/test/corpus/fixtures/2026-07-28/ClientCapabilities/elicitation-form-only-implicit.json @@ -0,0 +1,3 @@ +{ + "elicitation": {} +} diff --git a/packages/core/test/corpus/fixtures/2026-07-28/ClientCapabilities/extensions-ui-mime-types.json b/packages/core/test/corpus/fixtures/2026-07-28/ClientCapabilities/extensions-ui-mime-types.json new file mode 100644 index 0000000000..449bf29f5b --- /dev/null +++ b/packages/core/test/corpus/fixtures/2026-07-28/ClientCapabilities/extensions-ui-mime-types.json @@ -0,0 +1,7 @@ +{ + "extensions": { + "io.modelcontextprotocol/ui": { + "mimeTypes": ["text/html;profile=mcp-app"] + } + } +} diff --git a/packages/core/test/corpus/fixtures/2026-07-28/ClientCapabilities/roots-minimum-baseline-support.json b/packages/core/test/corpus/fixtures/2026-07-28/ClientCapabilities/roots-minimum-baseline-support.json new file mode 100644 index 0000000000..87a706ee4d --- /dev/null +++ b/packages/core/test/corpus/fixtures/2026-07-28/ClientCapabilities/roots-minimum-baseline-support.json @@ -0,0 +1,3 @@ +{ + "roots": {} +} diff --git a/packages/core/test/corpus/fixtures/2026-07-28/ClientCapabilities/sampling-context-inclusion-support-deprecated.json b/packages/core/test/corpus/fixtures/2026-07-28/ClientCapabilities/sampling-context-inclusion-support-deprecated.json new file mode 100644 index 0000000000..f6aba71c7b --- /dev/null +++ b/packages/core/test/corpus/fixtures/2026-07-28/ClientCapabilities/sampling-context-inclusion-support-deprecated.json @@ -0,0 +1,5 @@ +{ + "sampling": { + "context": {} + } +} \ No newline at end of file diff --git a/packages/core/test/corpus/fixtures/2026-07-28/ClientCapabilities/sampling-minimum-baseline-support.json b/packages/core/test/corpus/fixtures/2026-07-28/ClientCapabilities/sampling-minimum-baseline-support.json new file mode 100644 index 0000000000..5448e67a33 --- /dev/null +++ b/packages/core/test/corpus/fixtures/2026-07-28/ClientCapabilities/sampling-minimum-baseline-support.json @@ -0,0 +1,3 @@ +{ + "sampling": {} +} \ No newline at end of file diff --git a/packages/core/test/corpus/fixtures/2026-07-28/ClientCapabilities/sampling-tool-use-support.json b/packages/core/test/corpus/fixtures/2026-07-28/ClientCapabilities/sampling-tool-use-support.json new file mode 100644 index 0000000000..b269d8912c --- /dev/null +++ b/packages/core/test/corpus/fixtures/2026-07-28/ClientCapabilities/sampling-tool-use-support.json @@ -0,0 +1,5 @@ +{ + "sampling": { + "tools": {} + } +} \ No newline at end of file diff --git a/packages/core/test/corpus/fixtures/2026-07-28/CompleteRequest/completion-request.json b/packages/core/test/corpus/fixtures/2026-07-28/CompleteRequest/completion-request.json new file mode 100644 index 0000000000..f3c5a07417 --- /dev/null +++ b/packages/core/test/corpus/fixtures/2026-07-28/CompleteRequest/completion-request.json @@ -0,0 +1,23 @@ +{ + "jsonrpc": "2.0", + "id": "completion-example", + "method": "completion/complete", + "params": { + "_meta": { + "io.modelcontextprotocol/protocolVersion": "2026-07-28", + "io.modelcontextprotocol/clientInfo": { + "name": "ExampleClient", + "version": "1.0.0" + }, + "io.modelcontextprotocol/clientCapabilities": {} + }, + "ref": { + "type": "ref/prompt", + "name": "code_review" + }, + "argument": { + "name": "language", + "value": "py" + } + } +} diff --git a/packages/core/test/corpus/fixtures/2026-07-28/CompleteRequestParams/prompt-argument-completion-with-context.json b/packages/core/test/corpus/fixtures/2026-07-28/CompleteRequestParams/prompt-argument-completion-with-context.json new file mode 100644 index 0000000000..fb0f637793 --- /dev/null +++ b/packages/core/test/corpus/fixtures/2026-07-28/CompleteRequestParams/prompt-argument-completion-with-context.json @@ -0,0 +1,23 @@ +{ + "_meta": { + "io.modelcontextprotocol/protocolVersion": "2026-07-28", + "io.modelcontextprotocol/clientInfo": { + "name": "ExampleClient", + "version": "1.0.0" + }, + "io.modelcontextprotocol/clientCapabilities": {} + }, + "ref": { + "type": "ref/prompt", + "name": "code_review" + }, + "argument": { + "name": "framework", + "value": "fla" + }, + "context": { + "arguments": { + "language": "python" + } + } +} diff --git a/packages/core/test/corpus/fixtures/2026-07-28/CompleteRequestParams/prompt-argument-completion.json b/packages/core/test/corpus/fixtures/2026-07-28/CompleteRequestParams/prompt-argument-completion.json new file mode 100644 index 0000000000..af2bf84a08 --- /dev/null +++ b/packages/core/test/corpus/fixtures/2026-07-28/CompleteRequestParams/prompt-argument-completion.json @@ -0,0 +1,18 @@ +{ + "_meta": { + "io.modelcontextprotocol/protocolVersion": "2026-07-28", + "io.modelcontextprotocol/clientInfo": { + "name": "ExampleClient", + "version": "1.0.0" + }, + "io.modelcontextprotocol/clientCapabilities": {} + }, + "ref": { + "type": "ref/prompt", + "name": "code_review" + }, + "argument": { + "name": "language", + "value": "py" + } +} diff --git a/packages/core/test/corpus/fixtures/2026-07-28/CompleteResult/multiple-completion-values-with-more-available.json b/packages/core/test/corpus/fixtures/2026-07-28/CompleteResult/multiple-completion-values-with-more-available.json new file mode 100644 index 0000000000..c2f0633562 --- /dev/null +++ b/packages/core/test/corpus/fixtures/2026-07-28/CompleteResult/multiple-completion-values-with-more-available.json @@ -0,0 +1,8 @@ +{ + "resultType": "complete", + "completion": { + "values": ["python", "pytorch", "pyside"], + "total": 10, + "hasMore": true + } +} diff --git a/packages/core/test/corpus/fixtures/2026-07-28/CompleteResult/single-completion-value.json b/packages/core/test/corpus/fixtures/2026-07-28/CompleteResult/single-completion-value.json new file mode 100644 index 0000000000..36ec8985e5 --- /dev/null +++ b/packages/core/test/corpus/fixtures/2026-07-28/CompleteResult/single-completion-value.json @@ -0,0 +1,8 @@ +{ + "resultType": "complete", + "completion": { + "values": ["flask"], + "total": 1, + "hasMore": false + } +} diff --git a/packages/core/test/corpus/fixtures/2026-07-28/CompleteResultResponse/completion-result-response.json b/packages/core/test/corpus/fixtures/2026-07-28/CompleteResultResponse/completion-result-response.json new file mode 100644 index 0000000000..fb7156e5fa --- /dev/null +++ b/packages/core/test/corpus/fixtures/2026-07-28/CompleteResultResponse/completion-result-response.json @@ -0,0 +1,12 @@ +{ + "jsonrpc": "2.0", + "id": "completion-example", + "result": { + "resultType": "complete", + "completion": { + "values": ["flask"], + "total": 1, + "hasMore": false + } + } +} diff --git a/packages/core/test/corpus/fixtures/2026-07-28/CreateMessageRequest/sampling-request.json b/packages/core/test/corpus/fixtures/2026-07-28/CreateMessageRequest/sampling-request.json new file mode 100644 index 0000000000..70a17485f8 --- /dev/null +++ b/packages/core/test/corpus/fixtures/2026-07-28/CreateMessageRequest/sampling-request.json @@ -0,0 +1,25 @@ +{ + "method": "sampling/createMessage", + "params": { + "messages": [ + { + "role": "user", + "content": { + "type": "text", + "text": "What is the capital of France?" + } + } + ], + "modelPreferences": { + "hints": [ + { + "name": "claude-3-sonnet" + } + ], + "intelligencePriority": 0.8, + "speedPriority": 0.5 + }, + "systemPrompt": "You are a helpful assistant.", + "maxTokens": 100 + } +} diff --git a/packages/core/test/corpus/fixtures/2026-07-28/CreateMessageRequestParams/basic-request.json b/packages/core/test/corpus/fixtures/2026-07-28/CreateMessageRequestParams/basic-request.json new file mode 100644 index 0000000000..1c88a3f35f --- /dev/null +++ b/packages/core/test/corpus/fixtures/2026-07-28/CreateMessageRequestParams/basic-request.json @@ -0,0 +1,22 @@ +{ + "messages": [ + { + "role": "user", + "content": { + "type": "text", + "text": "What is the capital of France?" + } + } + ], + "modelPreferences": { + "hints": [ + { + "name": "claude-3-sonnet" + } + ], + "intelligencePriority": 0.8, + "speedPriority": 0.5 + }, + "systemPrompt": "You are a helpful assistant.", + "maxTokens": 100 +} \ No newline at end of file diff --git a/packages/core/test/corpus/fixtures/2026-07-28/CreateMessageRequestParams/follow-up-with-tool-results.json b/packages/core/test/corpus/fixtures/2026-07-28/CreateMessageRequestParams/follow-up-with-tool-results.json new file mode 100644 index 0000000000..cdea2d858d --- /dev/null +++ b/packages/core/test/corpus/fixtures/2026-07-28/CreateMessageRequestParams/follow-up-with-tool-results.json @@ -0,0 +1,67 @@ +{ + "messages": [ + { + "role": "user", + "content": { + "type": "text", + "text": "What's the weather like in Paris and London?" + } + }, + { + "role": "assistant", + "content": [ + { + "type": "tool_use", + "id": "call_abc123", + "name": "get_weather", + "input": { "city": "Paris" } + }, + { + "type": "tool_use", + "id": "call_def456", + "name": "get_weather", + "input": { "city": "London" } + } + ] + }, + { + "role": "user", + "content": [ + { + "type": "tool_result", + "toolUseId": "call_abc123", + "content": [ + { + "type": "text", + "text": "Weather in Paris: 18°C, partly cloudy" + } + ] + }, + { + "type": "tool_result", + "toolUseId": "call_def456", + "content": [ + { + "type": "text", + "text": "Weather in London: 15°C, rainy" + } + ] + } + ] + } + ], + "tools": [ + { + "name": "get_weather", + "description": "Get current weather for a city", + "inputSchema": { + "type": "object", + "properties": { + "city": { "type": "string" } + }, + "required": ["city"] + } + } + ], + "maxTokens": 1000 +} \ No newline at end of file diff --git a/packages/core/test/corpus/fixtures/2026-07-28/CreateMessageRequestParams/request-with-tools.json b/packages/core/test/corpus/fixtures/2026-07-28/CreateMessageRequestParams/request-with-tools.json new file mode 100644 index 0000000000..f79fd26ac9 --- /dev/null +++ b/packages/core/test/corpus/fixtures/2026-07-28/CreateMessageRequestParams/request-with-tools.json @@ -0,0 +1,31 @@ +{ + "messages": [ + { + "role": "user", + "content": { + "type": "text", + "text": "What's the weather like in Paris and London?" + } + } + ], + "tools": [ + { + "name": "get_weather", + "description": "Get current weather for a city", + "inputSchema": { + "type": "object", + "properties": { + "city": { + "type": "string", + "description": "City name" + } + }, + "required": ["city"] + } + } + ], + "toolChoice": { + "mode": "auto" + }, + "maxTokens": 1000 +} \ No newline at end of file diff --git a/packages/core/test/corpus/fixtures/2026-07-28/CreateMessageResult/final-response.json b/packages/core/test/corpus/fixtures/2026-07-28/CreateMessageResult/final-response.json new file mode 100644 index 0000000000..a9a457a8a8 --- /dev/null +++ b/packages/core/test/corpus/fixtures/2026-07-28/CreateMessageResult/final-response.json @@ -0,0 +1,9 @@ +{ + "role": "assistant", + "content": { + "type": "text", + "text": "Based on the current weather data:\n\n- **Paris**: 18°C and partly cloudy - quite pleasant!\n- **London**: 15°C and rainy - you'll want an umbrella.\n\nParis has slightly warmer and drier conditions today." + }, + "model": "claude-3-sonnet-20240307", + "stopReason": "endTurn" +} \ No newline at end of file diff --git a/packages/core/test/corpus/fixtures/2026-07-28/CreateMessageResult/text-response.json b/packages/core/test/corpus/fixtures/2026-07-28/CreateMessageResult/text-response.json new file mode 100644 index 0000000000..3b6f18dc7b --- /dev/null +++ b/packages/core/test/corpus/fixtures/2026-07-28/CreateMessageResult/text-response.json @@ -0,0 +1,9 @@ +{ + "role": "assistant", + "content": { + "type": "text", + "text": "The capital of France is Paris." + }, + "model": "claude-3-sonnet-20240307", + "stopReason": "endTurn" +} \ No newline at end of file diff --git a/packages/core/test/corpus/fixtures/2026-07-28/CreateMessageResult/tool-use-response.json b/packages/core/test/corpus/fixtures/2026-07-28/CreateMessageResult/tool-use-response.json new file mode 100644 index 0000000000..7599eee178 --- /dev/null +++ b/packages/core/test/corpus/fixtures/2026-07-28/CreateMessageResult/tool-use-response.json @@ -0,0 +1,23 @@ +{ + "role": "assistant", + "content": [ + { + "type": "tool_use", + "id": "call_abc123", + "name": "get_weather", + "input": { + "city": "Paris" + } + }, + { + "type": "tool_use", + "id": "call_def456", + "name": "get_weather", + "input": { + "city": "London" + } + } + ], + "model": "claude-3-sonnet-20240307", + "stopReason": "toolUse" +} \ No newline at end of file diff --git a/packages/core/test/corpus/fixtures/2026-07-28/DiscoverRequest/server-discover-request.json b/packages/core/test/corpus/fixtures/2026-07-28/DiscoverRequest/server-discover-request.json new file mode 100644 index 0000000000..85c7fe80f1 --- /dev/null +++ b/packages/core/test/corpus/fixtures/2026-07-28/DiscoverRequest/server-discover-request.json @@ -0,0 +1,15 @@ +{ + "jsonrpc": "2.0", + "id": "discover-1", + "method": "server/discover", + "params": { + "_meta": { + "io.modelcontextprotocol/protocolVersion": "2026-07-28", + "io.modelcontextprotocol/clientInfo": { + "name": "ExampleClient", + "version": "1.0.0" + }, + "io.modelcontextprotocol/clientCapabilities": {} + } + } +} diff --git a/packages/core/test/corpus/fixtures/2026-07-28/DiscoverResult/server-capabilities-discovery.json b/packages/core/test/corpus/fixtures/2026-07-28/DiscoverResult/server-capabilities-discovery.json new file mode 100644 index 0000000000..9f636318c4 --- /dev/null +++ b/packages/core/test/corpus/fixtures/2026-07-28/DiscoverResult/server-capabilities-discovery.json @@ -0,0 +1,15 @@ +{ + "resultType": "complete", + "supportedVersions": ["2026-07-28"], + "capabilities": { + "tools": {}, + "resources": {} + }, + "serverInfo": { + "name": "ExampleServer", + "version": "1.0.0" + }, + "instructions": "This server provides weather and resource utilities. Prefer `get_weather` for forecast lookups.", + "ttlMs": 3600000, + "cacheScope": "public" +} diff --git a/packages/core/test/corpus/fixtures/2026-07-28/DiscoverResultResponse/discover-result-response.json b/packages/core/test/corpus/fixtures/2026-07-28/DiscoverResultResponse/discover-result-response.json new file mode 100644 index 0000000000..1a162891bb --- /dev/null +++ b/packages/core/test/corpus/fixtures/2026-07-28/DiscoverResultResponse/discover-result-response.json @@ -0,0 +1,18 @@ +{ + "jsonrpc": "2.0", + "id": "discover-1", + "result": { + "resultType": "complete", + "supportedVersions": ["2026-07-28"], + "capabilities": { + "tools": {}, + "resources": {} + }, + "serverInfo": { + "name": "ExampleServer", + "version": "1.0.0" + }, + "ttlMs": 3600000, + "cacheScope": "public" + } +} diff --git a/packages/core/test/corpus/fixtures/2026-07-28/ElicitRequest/elicitation-request.json b/packages/core/test/corpus/fixtures/2026-07-28/ElicitRequest/elicitation-request.json new file mode 100644 index 0000000000..7c356f3556 --- /dev/null +++ b/packages/core/test/corpus/fixtures/2026-07-28/ElicitRequest/elicitation-request.json @@ -0,0 +1,18 @@ +{ + "method": "elicitation/create", + "params": { + "mode": "form", + "message": "Please provide your GitHub username", + "requestedSchema": { + "type": "object", + "properties": { + "name": { + "type": "string", + "title": "GitHub Username", + "description": "Your GitHub username" + } + }, + "required": ["name"] + } + } +} diff --git a/packages/core/test/corpus/fixtures/2026-07-28/ElicitRequestFormParams/elicit-multiple-fields.json b/packages/core/test/corpus/fixtures/2026-07-28/ElicitRequestFormParams/elicit-multiple-fields.json new file mode 100644 index 0000000000..7b8e0557c6 --- /dev/null +++ b/packages/core/test/corpus/fixtures/2026-07-28/ElicitRequestFormParams/elicit-multiple-fields.json @@ -0,0 +1,24 @@ +{ + "mode": "form", + "message": "Please provide your contact information", + "requestedSchema": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Your full name" + }, + "email": { + "type": "string", + "format": "email", + "description": "Your email address" + }, + "age": { + "type": "number", + "minimum": 18, + "description": "Your age" + } + }, + "required": ["name", "email"] + } +} diff --git a/packages/core/test/corpus/fixtures/2026-07-28/ElicitRequestFormParams/elicit-single-field.json b/packages/core/test/corpus/fixtures/2026-07-28/ElicitRequestFormParams/elicit-single-field.json new file mode 100644 index 0000000000..ea8fb43f64 --- /dev/null +++ b/packages/core/test/corpus/fixtures/2026-07-28/ElicitRequestFormParams/elicit-single-field.json @@ -0,0 +1,13 @@ +{ + "mode": "form", + "message": "Please provide your GitHub username", + "requestedSchema": { + "type": "object", + "properties": { + "name": { + "type": "string" + } + }, + "required": ["name"] + } +} diff --git a/packages/core/test/corpus/fixtures/2026-07-28/ElicitRequestURLParams/elicit-sensitive-data.json b/packages/core/test/corpus/fixtures/2026-07-28/ElicitRequestURLParams/elicit-sensitive-data.json new file mode 100644 index 0000000000..cf791ee3fe --- /dev/null +++ b/packages/core/test/corpus/fixtures/2026-07-28/ElicitRequestURLParams/elicit-sensitive-data.json @@ -0,0 +1,6 @@ +{ + "mode": "url", + "elicitationId": "550e8400-e29b-41d4-a716-446655440000", + "url": "https://mcp.example.com/ui/set_api_key", + "message": "Please provide your API key to continue." +} diff --git a/packages/core/test/corpus/fixtures/2026-07-28/ElicitResult/accept-url-mode-no-content.json b/packages/core/test/corpus/fixtures/2026-07-28/ElicitResult/accept-url-mode-no-content.json new file mode 100644 index 0000000000..ab47af78f3 --- /dev/null +++ b/packages/core/test/corpus/fixtures/2026-07-28/ElicitResult/accept-url-mode-no-content.json @@ -0,0 +1,3 @@ +{ + "action": "accept" +} diff --git a/packages/core/test/corpus/fixtures/2026-07-28/ElicitResult/input-multiple-fields.json b/packages/core/test/corpus/fixtures/2026-07-28/ElicitResult/input-multiple-fields.json new file mode 100644 index 0000000000..99b18e1990 --- /dev/null +++ b/packages/core/test/corpus/fixtures/2026-07-28/ElicitResult/input-multiple-fields.json @@ -0,0 +1,8 @@ +{ + "action": "accept", + "content": { + "name": "Monalisa Octocat", + "email": "octocat@github.com", + "age": 30 + } +} diff --git a/packages/core/test/corpus/fixtures/2026-07-28/ElicitResult/input-single-field.json b/packages/core/test/corpus/fixtures/2026-07-28/ElicitResult/input-single-field.json new file mode 100644 index 0000000000..4798da663e --- /dev/null +++ b/packages/core/test/corpus/fixtures/2026-07-28/ElicitResult/input-single-field.json @@ -0,0 +1,6 @@ +{ + "action": "accept", + "content": { + "name": "octocat" + } +} diff --git a/packages/core/test/corpus/fixtures/2026-07-28/ElicitationCompleteNotification/elicitation-complete.json b/packages/core/test/corpus/fixtures/2026-07-28/ElicitationCompleteNotification/elicitation-complete.json new file mode 100644 index 0000000000..bb6d564585 --- /dev/null +++ b/packages/core/test/corpus/fixtures/2026-07-28/ElicitationCompleteNotification/elicitation-complete.json @@ -0,0 +1,7 @@ +{ + "jsonrpc": "2.0", + "method": "notifications/elicitation/complete", + "params": { + "elicitationId": "550e8400-e29b-41d4-a716-446655440000" + } +} diff --git a/packages/core/test/corpus/fixtures/2026-07-28/EmbeddedResource/embedded-file-resource-with-annotations.json b/packages/core/test/corpus/fixtures/2026-07-28/EmbeddedResource/embedded-file-resource-with-annotations.json new file mode 100644 index 0000000000..01a8f2eb9f --- /dev/null +++ b/packages/core/test/corpus/fixtures/2026-07-28/EmbeddedResource/embedded-file-resource-with-annotations.json @@ -0,0 +1,13 @@ +{ + "type": "resource", + "resource": { + "uri": "file:///project/src/main.rs", + "mimeType": "text/x-rust", + "text": "fn main() {\n println!(\"Hello world!\");\n}" + }, + "annotations": { + "audience": ["user", "assistant"], + "priority": 0.7, + "lastModified": "2025-05-03T14:30:00Z" + } +} diff --git a/packages/core/test/corpus/fixtures/2026-07-28/GetPromptRequest/get-prompt-request.json b/packages/core/test/corpus/fixtures/2026-07-28/GetPromptRequest/get-prompt-request.json new file mode 100644 index 0000000000..b8af17c98d --- /dev/null +++ b/packages/core/test/corpus/fixtures/2026-07-28/GetPromptRequest/get-prompt-request.json @@ -0,0 +1,19 @@ +{ + "jsonrpc": "2.0", + "id": "get-prompt-example", + "method": "prompts/get", + "params": { + "_meta": { + "io.modelcontextprotocol/protocolVersion": "2026-07-28", + "io.modelcontextprotocol/clientInfo": { + "name": "ExampleClient", + "version": "1.0.0" + }, + "io.modelcontextprotocol/clientCapabilities": {} + }, + "name": "code_review", + "arguments": { + "code": "def hello():\n print('world')" + } + } +} diff --git a/packages/core/test/corpus/fixtures/2026-07-28/GetPromptRequestParams/get-code-review-prompt.json b/packages/core/test/corpus/fixtures/2026-07-28/GetPromptRequestParams/get-code-review-prompt.json new file mode 100644 index 0000000000..0bfdf3b01b --- /dev/null +++ b/packages/core/test/corpus/fixtures/2026-07-28/GetPromptRequestParams/get-code-review-prompt.json @@ -0,0 +1,14 @@ +{ + "_meta": { + "io.modelcontextprotocol/protocolVersion": "2026-07-28", + "io.modelcontextprotocol/clientInfo": { + "name": "ExampleClient", + "version": "1.0.0" + }, + "io.modelcontextprotocol/clientCapabilities": {} + }, + "name": "code_review", + "arguments": { + "code": "def hello():\n print('world')" + } +} diff --git a/packages/core/test/corpus/fixtures/2026-07-28/GetPromptResult/code-review-prompt.json b/packages/core/test/corpus/fixtures/2026-07-28/GetPromptResult/code-review-prompt.json new file mode 100644 index 0000000000..cb3518aee4 --- /dev/null +++ b/packages/core/test/corpus/fixtures/2026-07-28/GetPromptResult/code-review-prompt.json @@ -0,0 +1,13 @@ +{ + "resultType": "complete", + "description": "Code review prompt", + "messages": [ + { + "role": "user", + "content": { + "type": "text", + "text": "Please review this Python code:\ndef hello():\n print('world')" + } + } + ] +} diff --git a/packages/core/test/corpus/fixtures/2026-07-28/GetPromptResultResponse/get-prompt-result-response.json b/packages/core/test/corpus/fixtures/2026-07-28/GetPromptResultResponse/get-prompt-result-response.json new file mode 100644 index 0000000000..a257ccf9ed --- /dev/null +++ b/packages/core/test/corpus/fixtures/2026-07-28/GetPromptResultResponse/get-prompt-result-response.json @@ -0,0 +1,17 @@ +{ + "jsonrpc": "2.0", + "id": "get-prompt-example", + "result": { + "resultType": "complete", + "description": "Code review prompt", + "messages": [ + { + "role": "user", + "content": { + "type": "text", + "text": "Please review this Python code:\ndef hello():\n print('world')" + } + } + ] + } +} diff --git a/packages/core/test/corpus/fixtures/2026-07-28/ImageContent/image-png-content-with-annotations.json b/packages/core/test/corpus/fixtures/2026-07-28/ImageContent/image-png-content-with-annotations.json new file mode 100644 index 0000000000..32f8ef683e --- /dev/null +++ b/packages/core/test/corpus/fixtures/2026-07-28/ImageContent/image-png-content-with-annotations.json @@ -0,0 +1,9 @@ +{ + "type": "image", + "data": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==", + "mimeType": "image/png", + "annotations": { + "audience": ["user"], + "priority": 0.9 + } +} diff --git a/packages/core/test/corpus/fixtures/2026-07-28/InputRequests/elicitation-and-sampling-input-requests.json b/packages/core/test/corpus/fixtures/2026-07-28/InputRequests/elicitation-and-sampling-input-requests.json new file mode 100644 index 0000000000..5d1ce974aa --- /dev/null +++ b/packages/core/test/corpus/fixtures/2026-07-28/InputRequests/elicitation-and-sampling-input-requests.json @@ -0,0 +1,33 @@ +{ + "github_login": { + "method": "elicitation/create", + "params": { + "mode": "form", + "message": "Please provide your GitHub username", + "requestedSchema": { + "type": "object", + "properties": { + "name": { + "type": "string" + } + }, + "required": ["name"] + } + } + }, + "capital_of_france": { + "method": "sampling/createMessage", + "params": { + "messages": [ + { + "role": "user", + "content": { + "type": "text", + "text": "What is the capital of France?" + } + } + ], + "maxTokens": 100 + } + } +} diff --git a/packages/core/test/corpus/fixtures/2026-07-28/InputRequiredResult/input-required-result-with-elicitation-and-sampling-and-request-state.json b/packages/core/test/corpus/fixtures/2026-07-28/InputRequiredResult/input-required-result-with-elicitation-and-sampling-and-request-state.json new file mode 100644 index 0000000000..6ffc953944 --- /dev/null +++ b/packages/core/test/corpus/fixtures/2026-07-28/InputRequiredResult/input-required-result-with-elicitation-and-sampling-and-request-state.json @@ -0,0 +1,36 @@ +{ + "resultType": "input_required", + "inputRequests": { + "github_login": { + "method": "elicitation/create", + "params": { + "message": "Please provide your GitHub username", + "requestedSchema": { + "type": "object", + "properties": { + "name": { + "type": "string" + } + }, + "required": ["name"] + } + } + }, + "capital_of_france": { + "method": "sampling/createMessage", + "params": { + "messages": [ + { + "role": "user", + "content": { + "type": "text", + "text": "What is the capital of France?" + } + } + ], + "maxTokens": 100 + } + } + }, + "requestState": "eyJsb2NhdGlvbiI6Ik5ldyBZb3JrIn0" +} diff --git a/packages/core/test/corpus/fixtures/2026-07-28/InputRequiredResult/input-required-result-with-request-state-only.json b/packages/core/test/corpus/fixtures/2026-07-28/InputRequiredResult/input-required-result-with-request-state-only.json new file mode 100644 index 0000000000..7f1dec1f69 --- /dev/null +++ b/packages/core/test/corpus/fixtures/2026-07-28/InputRequiredResult/input-required-result-with-request-state-only.json @@ -0,0 +1,4 @@ +{ + "resultType": "input_required", + "requestState": "eyJwcm9ncmVzcyI6IjUwJSIsInN0YXRlIjoicHJvY2Vzc2luZyJ9" +} diff --git a/packages/core/test/corpus/fixtures/2026-07-28/InputResponses/elicitation-and-sampling-input-responses.json b/packages/core/test/corpus/fixtures/2026-07-28/InputResponses/elicitation-and-sampling-input-responses.json new file mode 100644 index 0000000000..1f5cbcf0d6 --- /dev/null +++ b/packages/core/test/corpus/fixtures/2026-07-28/InputResponses/elicitation-and-sampling-input-responses.json @@ -0,0 +1,17 @@ +{ + "github_login": { + "action": "accept", + "content": { + "name": "octocat" + } + }, + "capital_of_france": { + "role": "assistant", + "content": { + "type": "text", + "text": "The capital of France is Paris." + }, + "model": "claude-3-sonnet-20240307", + "stopReason": "endTurn" + } +} diff --git a/packages/core/test/corpus/fixtures/2026-07-28/InternalError/unexpected-error.json b/packages/core/test/corpus/fixtures/2026-07-28/InternalError/unexpected-error.json new file mode 100644 index 0000000000..2560c88d32 --- /dev/null +++ b/packages/core/test/corpus/fixtures/2026-07-28/InternalError/unexpected-error.json @@ -0,0 +1,4 @@ +{ + "code": -32603, + "message": "Internal error" +} diff --git a/packages/core/test/corpus/fixtures/2026-07-28/InvalidParamsError/invalid-cursor.json b/packages/core/test/corpus/fixtures/2026-07-28/InvalidParamsError/invalid-cursor.json new file mode 100644 index 0000000000..674bb5422d --- /dev/null +++ b/packages/core/test/corpus/fixtures/2026-07-28/InvalidParamsError/invalid-cursor.json @@ -0,0 +1,4 @@ +{ + "code": -32602, + "message": "Invalid cursor" +} diff --git a/packages/core/test/corpus/fixtures/2026-07-28/InvalidParamsError/invalid-tool-arguments.json b/packages/core/test/corpus/fixtures/2026-07-28/InvalidParamsError/invalid-tool-arguments.json new file mode 100644 index 0000000000..afa93c8b4a --- /dev/null +++ b/packages/core/test/corpus/fixtures/2026-07-28/InvalidParamsError/invalid-tool-arguments.json @@ -0,0 +1,4 @@ +{ + "code": -32602, + "message": "Invalid arguments for tool calculate: Missing required property 'expression'" +} diff --git a/packages/core/test/corpus/fixtures/2026-07-28/InvalidParamsError/unknown-prompt.json b/packages/core/test/corpus/fixtures/2026-07-28/InvalidParamsError/unknown-prompt.json new file mode 100644 index 0000000000..741e88b0d7 --- /dev/null +++ b/packages/core/test/corpus/fixtures/2026-07-28/InvalidParamsError/unknown-prompt.json @@ -0,0 +1,4 @@ +{ + "code": -32602, + "message": "Unknown prompt: invalid_prompt_name" +} diff --git a/packages/core/test/corpus/fixtures/2026-07-28/InvalidParamsError/unknown-tool.json b/packages/core/test/corpus/fixtures/2026-07-28/InvalidParamsError/unknown-tool.json new file mode 100644 index 0000000000..bde98fa520 --- /dev/null +++ b/packages/core/test/corpus/fixtures/2026-07-28/InvalidParamsError/unknown-tool.json @@ -0,0 +1,4 @@ +{ + "code": -32602, + "message": "Unknown tool: invalid_tool_name" +} diff --git a/packages/core/test/corpus/fixtures/2026-07-28/ListPromptsRequest/list-prompts-request.json b/packages/core/test/corpus/fixtures/2026-07-28/ListPromptsRequest/list-prompts-request.json new file mode 100644 index 0000000000..9fa881f332 --- /dev/null +++ b/packages/core/test/corpus/fixtures/2026-07-28/ListPromptsRequest/list-prompts-request.json @@ -0,0 +1,15 @@ +{ + "jsonrpc": "2.0", + "id": "list-prompts-example", + "method": "prompts/list", + "params": { + "_meta": { + "io.modelcontextprotocol/protocolVersion": "2026-07-28", + "io.modelcontextprotocol/clientInfo": { + "name": "ExampleClient", + "version": "1.0.0" + }, + "io.modelcontextprotocol/clientCapabilities": {} + } + } +} diff --git a/packages/core/test/corpus/fixtures/2026-07-28/ListPromptsResult/prompts-list-with-cursor-and-ttl.json b/packages/core/test/corpus/fixtures/2026-07-28/ListPromptsResult/prompts-list-with-cursor-and-ttl.json new file mode 100644 index 0000000000..1d841baa83 --- /dev/null +++ b/packages/core/test/corpus/fixtures/2026-07-28/ListPromptsResult/prompts-list-with-cursor-and-ttl.json @@ -0,0 +1,27 @@ +{ + "resultType": "complete", + "prompts": [ + { + "name": "code_review", + "title": "Request Code Review", + "description": "Asks the LLM to analyze code quality and suggest improvements", + "arguments": [ + { + "name": "code", + "description": "The code to review", + "required": true + } + ], + "icons": [ + { + "src": "https://example.com/review-icon.svg", + "mimeType": "image/svg+xml", + "sizes": ["any"] + } + ] + } + ], + "nextCursor": "next-page-cursor", + "ttlMs": 600000, + "cacheScope": "public" +} diff --git a/packages/core/test/corpus/fixtures/2026-07-28/ListPromptsResultResponse/list-prompts-result-response.json b/packages/core/test/corpus/fixtures/2026-07-28/ListPromptsResultResponse/list-prompts-result-response.json new file mode 100644 index 0000000000..61118cab64 --- /dev/null +++ b/packages/core/test/corpus/fixtures/2026-07-28/ListPromptsResultResponse/list-prompts-result-response.json @@ -0,0 +1,31 @@ +{ + "jsonrpc": "2.0", + "id": "list-prompts-example", + "result": { + "resultType": "complete", + "prompts": [ + { + "name": "code_review", + "title": "Request Code Review", + "description": "Asks the LLM to analyze code quality and suggest improvements", + "arguments": [ + { + "name": "code", + "description": "The code to review", + "required": true + } + ], + "icons": [ + { + "src": "https://example.com/review-icon.svg", + "mimeType": "image/svg+xml", + "sizes": ["any"] + } + ] + } + ], + "nextCursor": "next-page-cursor", + "ttlMs": 600000, + "cacheScope": "public" + } +} diff --git a/packages/core/test/corpus/fixtures/2026-07-28/ListResourceTemplatesRequest/list-resource-templates-request.json b/packages/core/test/corpus/fixtures/2026-07-28/ListResourceTemplatesRequest/list-resource-templates-request.json new file mode 100644 index 0000000000..13917c5911 --- /dev/null +++ b/packages/core/test/corpus/fixtures/2026-07-28/ListResourceTemplatesRequest/list-resource-templates-request.json @@ -0,0 +1,15 @@ +{ + "jsonrpc": "2.0", + "id": "list-resource-templates-example", + "method": "resources/templates/list", + "params": { + "_meta": { + "io.modelcontextprotocol/protocolVersion": "2026-07-28", + "io.modelcontextprotocol/clientInfo": { + "name": "ExampleClient", + "version": "1.0.0" + }, + "io.modelcontextprotocol/clientCapabilities": {} + } + } +} diff --git a/packages/core/test/corpus/fixtures/2026-07-28/ListResourceTemplatesResult/resource-templates-list-with-cursor-and-ttl.json b/packages/core/test/corpus/fixtures/2026-07-28/ListResourceTemplatesResult/resource-templates-list-with-cursor-and-ttl.json new file mode 100644 index 0000000000..7abd62b150 --- /dev/null +++ b/packages/core/test/corpus/fixtures/2026-07-28/ListResourceTemplatesResult/resource-templates-list-with-cursor-and-ttl.json @@ -0,0 +1,22 @@ +{ + "resultType": "complete", + "resourceTemplates": [ + { + "uriTemplate": "file:///{path}", + "name": "Project Files", + "title": "📁 Project Files", + "description": "Access files in the project directory", + "mimeType": "application/octet-stream", + "icons": [ + { + "src": "https://example.com/folder-icon.png", + "mimeType": "image/png", + "sizes": ["48x48"] + } + ] + } + ], + "nextCursor": "next-page-cursor", + "ttlMs": 3600000, + "cacheScope": "public" +} diff --git a/packages/core/test/corpus/fixtures/2026-07-28/ListResourceTemplatesResultResponse/list-resource-templates-result-response.json b/packages/core/test/corpus/fixtures/2026-07-28/ListResourceTemplatesResultResponse/list-resource-templates-result-response.json new file mode 100644 index 0000000000..3fda957c35 --- /dev/null +++ b/packages/core/test/corpus/fixtures/2026-07-28/ListResourceTemplatesResultResponse/list-resource-templates-result-response.json @@ -0,0 +1,25 @@ +{ + "jsonrpc": "2.0", + "id": "list-resource-templates-example", + "result": { + "resultType": "complete", + "resourceTemplates": [ + { + "uriTemplate": "file:///{path}", + "name": "Project Files", + "title": "Project Files", + "description": "Access files in the project directory", + "mimeType": "application/octet-stream", + "icons": [ + { + "src": "https://example.com/folder-icon.png", + "mimeType": "image/png", + "sizes": ["48x48"] + } + ] + } + ], + "ttlMs": 3600000, + "cacheScope": "public" + } +} diff --git a/packages/core/test/corpus/fixtures/2026-07-28/ListResourcesRequest/list-resources-request.json b/packages/core/test/corpus/fixtures/2026-07-28/ListResourcesRequest/list-resources-request.json new file mode 100644 index 0000000000..0ce2f47025 --- /dev/null +++ b/packages/core/test/corpus/fixtures/2026-07-28/ListResourcesRequest/list-resources-request.json @@ -0,0 +1,15 @@ +{ + "jsonrpc": "2.0", + "id": "list-resources-example", + "method": "resources/list", + "params": { + "_meta": { + "io.modelcontextprotocol/protocolVersion": "2026-07-28", + "io.modelcontextprotocol/clientInfo": { + "name": "ExampleClient", + "version": "1.0.0" + }, + "io.modelcontextprotocol/clientCapabilities": {} + } + } +} diff --git a/packages/core/test/corpus/fixtures/2026-07-28/ListResourcesResult/resources-list-with-cursor-and-ttl.json b/packages/core/test/corpus/fixtures/2026-07-28/ListResourcesResult/resources-list-with-cursor-and-ttl.json new file mode 100644 index 0000000000..e701fc6421 --- /dev/null +++ b/packages/core/test/corpus/fixtures/2026-07-28/ListResourcesResult/resources-list-with-cursor-and-ttl.json @@ -0,0 +1,22 @@ +{ + "resultType": "complete", + "resources": [ + { + "uri": "file:///project/src/main.rs", + "name": "main.rs", + "title": "Rust Software Application Main File", + "description": "Primary application entry point", + "mimeType": "text/x-rust", + "icons": [ + { + "src": "https://example.com/rust-file-icon.png", + "mimeType": "image/png", + "sizes": ["48x48"] + } + ] + } + ], + "nextCursor": "eyJwYWdlIjogM30=", + "ttlMs": 600000, + "cacheScope": "private" +} diff --git a/packages/core/test/corpus/fixtures/2026-07-28/ListResourcesResultResponse/list-resources-result-response.json b/packages/core/test/corpus/fixtures/2026-07-28/ListResourcesResultResponse/list-resources-result-response.json new file mode 100644 index 0000000000..e17cc50111 --- /dev/null +++ b/packages/core/test/corpus/fixtures/2026-07-28/ListResourcesResultResponse/list-resources-result-response.json @@ -0,0 +1,26 @@ +{ + "jsonrpc": "2.0", + "id": "list-resources-example", + "result": { + "resultType": "complete", + "resources": [ + { + "uri": "file:///project/src/main.rs", + "name": "main.rs", + "title": "Rust Software Application Main File", + "description": "Primary application entry point", + "mimeType": "text/x-rust", + "icons": [ + { + "src": "https://example.com/rust-file-icon.png", + "mimeType": "image/png", + "sizes": ["48x48"] + } + ] + } + ], + "nextCursor": "eyJwYWdlIjogM30=", + "ttlMs": 600000, + "cacheScope": "private" + } +} diff --git a/packages/core/test/corpus/fixtures/2026-07-28/ListRootsRequest/list-roots-request.json b/packages/core/test/corpus/fixtures/2026-07-28/ListRootsRequest/list-roots-request.json new file mode 100644 index 0000000000..ef0b0c0c6a --- /dev/null +++ b/packages/core/test/corpus/fixtures/2026-07-28/ListRootsRequest/list-roots-request.json @@ -0,0 +1,4 @@ +{ + "id": "list-roots-example", + "method": "roots/list" +} diff --git a/packages/core/test/corpus/fixtures/2026-07-28/ListRootsResult/multiple-root-directories.json b/packages/core/test/corpus/fixtures/2026-07-28/ListRootsResult/multiple-root-directories.json new file mode 100644 index 0000000000..0cf0e78c2d --- /dev/null +++ b/packages/core/test/corpus/fixtures/2026-07-28/ListRootsResult/multiple-root-directories.json @@ -0,0 +1,12 @@ +{ + "roots": [ + { + "uri": "file:///home/user/repos/frontend", + "name": "Frontend Repository" + }, + { + "uri": "file:///home/user/repos/backend", + "name": "Backend Repository" + } + ] +} diff --git a/packages/core/test/corpus/fixtures/2026-07-28/ListRootsResult/single-root-directory.json b/packages/core/test/corpus/fixtures/2026-07-28/ListRootsResult/single-root-directory.json new file mode 100644 index 0000000000..0ea6963dcd --- /dev/null +++ b/packages/core/test/corpus/fixtures/2026-07-28/ListRootsResult/single-root-directory.json @@ -0,0 +1,8 @@ +{ + "roots": [ + { + "uri": "file:///home/user/projects/myproject", + "name": "My Project" + } + ] +} diff --git a/packages/core/test/corpus/fixtures/2026-07-28/ListToolsRequest/list-tools-request.json b/packages/core/test/corpus/fixtures/2026-07-28/ListToolsRequest/list-tools-request.json new file mode 100644 index 0000000000..02e93eb771 --- /dev/null +++ b/packages/core/test/corpus/fixtures/2026-07-28/ListToolsRequest/list-tools-request.json @@ -0,0 +1,15 @@ +{ + "jsonrpc": "2.0", + "id": "list-tools-example", + "method": "tools/list", + "params": { + "_meta": { + "io.modelcontextprotocol/protocolVersion": "2026-07-28", + "io.modelcontextprotocol/clientInfo": { + "name": "ExampleClient", + "version": "1.0.0" + }, + "io.modelcontextprotocol/clientCapabilities": {} + } + } +} diff --git a/packages/core/test/corpus/fixtures/2026-07-28/ListToolsResult/tools-list-with-cursor-and-ttl.json b/packages/core/test/corpus/fixtures/2026-07-28/ListToolsResult/tools-list-with-cursor-and-ttl.json new file mode 100644 index 0000000000..b81f02d4c9 --- /dev/null +++ b/packages/core/test/corpus/fixtures/2026-07-28/ListToolsResult/tools-list-with-cursor-and-ttl.json @@ -0,0 +1,30 @@ +{ + "resultType": "complete", + "tools": [ + { + "name": "get_weather", + "title": "Weather Information Provider", + "description": "Get current weather information for a location", + "inputSchema": { + "type": "object", + "properties": { + "location": { + "type": "string", + "description": "City name or zip code" + } + }, + "required": ["location"] + }, + "icons": [ + { + "src": "https://example.com/weather-icon.png", + "mimeType": "image/png", + "sizes": ["48x48"] + } + ] + } + ], + "nextCursor": "next-page-cursor", + "ttlMs": 300000, + "cacheScope": "public" +} diff --git a/packages/core/test/corpus/fixtures/2026-07-28/ListToolsResultResponse/list-tools-result-response.json b/packages/core/test/corpus/fixtures/2026-07-28/ListToolsResultResponse/list-tools-result-response.json new file mode 100644 index 0000000000..1e0c84f9ef --- /dev/null +++ b/packages/core/test/corpus/fixtures/2026-07-28/ListToolsResultResponse/list-tools-result-response.json @@ -0,0 +1,34 @@ +{ + "jsonrpc": "2.0", + "id": "list-tools-example", + "result": { + "resultType": "complete", + "tools": [ + { + "name": "get_weather", + "title": "Weather Information Provider", + "description": "Get current weather information for a location", + "inputSchema": { + "type": "object", + "properties": { + "location": { + "type": "string", + "description": "City name or zip code" + } + }, + "required": ["location"] + }, + "icons": [ + { + "src": "https://example.com/weather-icon.png", + "mimeType": "image/png", + "sizes": ["48x48"] + } + ] + } + ], + "nextCursor": "next-page-cursor", + "ttlMs": 3600000, + "cacheScope": "public" + } +} diff --git a/packages/core/test/corpus/fixtures/2026-07-28/LoggingMessageNotification/log-database-connection-failed.json b/packages/core/test/corpus/fixtures/2026-07-28/LoggingMessageNotification/log-database-connection-failed.json new file mode 100644 index 0000000000..7c131e04bb --- /dev/null +++ b/packages/core/test/corpus/fixtures/2026-07-28/LoggingMessageNotification/log-database-connection-failed.json @@ -0,0 +1,15 @@ +{ + "jsonrpc": "2.0", + "method": "notifications/message", + "params": { + "level": "error", + "logger": "database", + "data": { + "error": "Connection failed", + "details": { + "host": "localhost", + "port": 5432 + } + } + } +} diff --git a/packages/core/test/corpus/fixtures/2026-07-28/LoggingMessageNotificationParams/log-database-connection-failed.json b/packages/core/test/corpus/fixtures/2026-07-28/LoggingMessageNotificationParams/log-database-connection-failed.json new file mode 100644 index 0000000000..dad2430eec --- /dev/null +++ b/packages/core/test/corpus/fixtures/2026-07-28/LoggingMessageNotificationParams/log-database-connection-failed.json @@ -0,0 +1,11 @@ +{ + "level": "error", + "logger": "database", + "data": { + "error": "Connection failed", + "details": { + "host": "localhost", + "port": 5432 + } + } +} diff --git a/packages/core/test/corpus/fixtures/2026-07-28/MethodNotFoundError/prompts-not-supported.json b/packages/core/test/corpus/fixtures/2026-07-28/MethodNotFoundError/prompts-not-supported.json new file mode 100644 index 0000000000..0a025a1cd1 --- /dev/null +++ b/packages/core/test/corpus/fixtures/2026-07-28/MethodNotFoundError/prompts-not-supported.json @@ -0,0 +1,7 @@ +{ + "code": -32601, + "message": "Prompts not supported", + "data": { + "reason": "Server does not support the prompts capability" + } +} diff --git a/packages/core/test/corpus/fixtures/2026-07-28/MissingRequiredClientCapabilityError/missing-elicitation-capability.json b/packages/core/test/corpus/fixtures/2026-07-28/MissingRequiredClientCapabilityError/missing-elicitation-capability.json new file mode 100644 index 0000000000..10917d3603 --- /dev/null +++ b/packages/core/test/corpus/fixtures/2026-07-28/MissingRequiredClientCapabilityError/missing-elicitation-capability.json @@ -0,0 +1,13 @@ +{ + "jsonrpc": "2.0", + "id": 1, + "error": { + "code": -32003, + "message": "Server requires the elicitation capability for this request", + "data": { + "requiredCapabilities": { + "elicitation": {} + } + } + } +} diff --git a/packages/core/test/corpus/fixtures/2026-07-28/ModelPreferences/with-hints-and-priorities.json b/packages/core/test/corpus/fixtures/2026-07-28/ModelPreferences/with-hints-and-priorities.json new file mode 100644 index 0000000000..44786871db --- /dev/null +++ b/packages/core/test/corpus/fixtures/2026-07-28/ModelPreferences/with-hints-and-priorities.json @@ -0,0 +1,9 @@ +{ + "hints": [ + { "name": "claude-3-sonnet" }, + { "name": "claude" } + ], + "costPriority": 0.3, + "speedPriority": 0.8, + "intelligencePriority": 0.5 +} \ No newline at end of file diff --git a/packages/core/test/corpus/fixtures/2026-07-28/NumberSchema/number-input-schema.json b/packages/core/test/corpus/fixtures/2026-07-28/NumberSchema/number-input-schema.json new file mode 100644 index 0000000000..6049ed6636 --- /dev/null +++ b/packages/core/test/corpus/fixtures/2026-07-28/NumberSchema/number-input-schema.json @@ -0,0 +1,8 @@ +{ + "type": "number", + "title": "Display Name", + "description": "Description text", + "minimum": 0, + "maximum": 100, + "default": 50 +} diff --git a/packages/core/test/corpus/fixtures/2026-07-28/PaginatedRequestParams/list-with-cursor.json b/packages/core/test/corpus/fixtures/2026-07-28/PaginatedRequestParams/list-with-cursor.json new file mode 100644 index 0000000000..948178be8d --- /dev/null +++ b/packages/core/test/corpus/fixtures/2026-07-28/PaginatedRequestParams/list-with-cursor.json @@ -0,0 +1,11 @@ +{ + "_meta": { + "io.modelcontextprotocol/protocolVersion": "2026-07-28", + "io.modelcontextprotocol/clientInfo": { + "name": "ExampleClient", + "version": "1.0.0" + }, + "io.modelcontextprotocol/clientCapabilities": {} + }, + "cursor": "eyJwYWdlIjogMn0=" +} diff --git a/packages/core/test/corpus/fixtures/2026-07-28/ParseError/invalid-json.json b/packages/core/test/corpus/fixtures/2026-07-28/ParseError/invalid-json.json new file mode 100644 index 0000000000..eb47719580 --- /dev/null +++ b/packages/core/test/corpus/fixtures/2026-07-28/ParseError/invalid-json.json @@ -0,0 +1,4 @@ +{ + "code": -32700, + "message": "Parse error: Invalid JSON" +} diff --git a/packages/core/test/corpus/fixtures/2026-07-28/ProgressNotification/progress-message.json b/packages/core/test/corpus/fixtures/2026-07-28/ProgressNotification/progress-message.json new file mode 100644 index 0000000000..1e66088b23 --- /dev/null +++ b/packages/core/test/corpus/fixtures/2026-07-28/ProgressNotification/progress-message.json @@ -0,0 +1,10 @@ +{ + "jsonrpc": "2.0", + "method": "notifications/progress", + "params": { + "progressToken": "oivaizmir", + "progress": 50, + "total": 100, + "message": "Reticulating splines..." + } +} diff --git a/packages/core/test/corpus/fixtures/2026-07-28/ProgressNotificationParams/progress-message.json b/packages/core/test/corpus/fixtures/2026-07-28/ProgressNotificationParams/progress-message.json new file mode 100644 index 0000000000..49549c115f --- /dev/null +++ b/packages/core/test/corpus/fixtures/2026-07-28/ProgressNotificationParams/progress-message.json @@ -0,0 +1,6 @@ +{ + "progressToken": "oivaizmir", + "progress": 50, + "total": 100, + "message": "Reticulating splines..." +} diff --git a/packages/core/test/corpus/fixtures/2026-07-28/PromptListChangedNotification/prompts-list-changed.json b/packages/core/test/corpus/fixtures/2026-07-28/PromptListChangedNotification/prompts-list-changed.json new file mode 100644 index 0000000000..858cd5d874 --- /dev/null +++ b/packages/core/test/corpus/fixtures/2026-07-28/PromptListChangedNotification/prompts-list-changed.json @@ -0,0 +1,4 @@ +{ + "jsonrpc": "2.0", + "method": "notifications/prompts/list_changed" +} diff --git a/packages/core/test/corpus/fixtures/2026-07-28/ReadResourceRequest/read-resource-request.json b/packages/core/test/corpus/fixtures/2026-07-28/ReadResourceRequest/read-resource-request.json new file mode 100644 index 0000000000..073a816eb6 --- /dev/null +++ b/packages/core/test/corpus/fixtures/2026-07-28/ReadResourceRequest/read-resource-request.json @@ -0,0 +1,16 @@ +{ + "jsonrpc": "2.0", + "id": "read-resource-example", + "method": "resources/read", + "params": { + "_meta": { + "io.modelcontextprotocol/protocolVersion": "2026-07-28", + "io.modelcontextprotocol/clientInfo": { + "name": "ExampleClient", + "version": "1.0.0" + }, + "io.modelcontextprotocol/clientCapabilities": {} + }, + "uri": "file:///project/src/main.rs" + } +} diff --git a/packages/core/test/corpus/fixtures/2026-07-28/ReadResourceResult/file-resource-contents.json b/packages/core/test/corpus/fixtures/2026-07-28/ReadResourceResult/file-resource-contents.json new file mode 100644 index 0000000000..591fd09ce9 --- /dev/null +++ b/packages/core/test/corpus/fixtures/2026-07-28/ReadResourceResult/file-resource-contents.json @@ -0,0 +1,12 @@ +{ + "resultType": "complete", + "contents": [ + { + "uri": "file:///project/src/main.rs", + "mimeType": "text/x-rust", + "text": "fn main() {\n println!(\"Hello world!\");\n}" + } + ], + "ttlMs": 60000, + "cacheScope": "private" +} diff --git a/packages/core/test/corpus/fixtures/2026-07-28/ReadResourceResultResponse/read-resource-result-response-with-ttl.json b/packages/core/test/corpus/fixtures/2026-07-28/ReadResourceResultResponse/read-resource-result-response-with-ttl.json new file mode 100644 index 0000000000..b63f398a16 --- /dev/null +++ b/packages/core/test/corpus/fixtures/2026-07-28/ReadResourceResultResponse/read-resource-result-response-with-ttl.json @@ -0,0 +1,16 @@ +{ + "jsonrpc": "2.0", + "id": "read-resource-with-ttl-example", + "result": { + "resultType": "complete", + "contents": [ + { + "uri": "file:///project/src/main.rs", + "mimeType": "text/x-rust", + "text": "fn main() {\n println!(\"Hello world!\");\n}" + } + ], + "ttlMs": 60000, + "cacheScope": "private" + } +} diff --git a/packages/core/test/corpus/fixtures/2026-07-28/ReadResourceResultResponse/read-resource-result-response.json b/packages/core/test/corpus/fixtures/2026-07-28/ReadResourceResultResponse/read-resource-result-response.json new file mode 100644 index 0000000000..93bfae6943 --- /dev/null +++ b/packages/core/test/corpus/fixtures/2026-07-28/ReadResourceResultResponse/read-resource-result-response.json @@ -0,0 +1,14 @@ +{ + "jsonrpc": "2.0", + "id": "read-resource-example", + "result": { + "resultType": "complete", + "contents": [ + { + "uri": "file:///project/src/main.rs", + "mimeType": "text/x-rust", + "text": "fn main() {\n println!(\"Hello world!\");\n}" + } + ] + } +} diff --git a/packages/core/test/corpus/fixtures/2026-07-28/Resource/file-resource-with-annotations.json b/packages/core/test/corpus/fixtures/2026-07-28/Resource/file-resource-with-annotations.json new file mode 100644 index 0000000000..3e268afb1d --- /dev/null +++ b/packages/core/test/corpus/fixtures/2026-07-28/Resource/file-resource-with-annotations.json @@ -0,0 +1,11 @@ +{ + "uri": "file:///project/README.md", + "name": "README.md", + "title": "Project Documentation", + "mimeType": "text/markdown", + "annotations": { + "audience": ["user"], + "priority": 0.8, + "lastModified": "2025-01-12T15:00:58Z" + } +} diff --git a/packages/core/test/corpus/fixtures/2026-07-28/ResourceLink/file-resource-link.json b/packages/core/test/corpus/fixtures/2026-07-28/ResourceLink/file-resource-link.json new file mode 100644 index 0000000000..d35682596f --- /dev/null +++ b/packages/core/test/corpus/fixtures/2026-07-28/ResourceLink/file-resource-link.json @@ -0,0 +1,7 @@ +{ + "type": "resource_link", + "uri": "file:///project/src/main.rs", + "name": "main.rs", + "description": "Primary application entry point", + "mimeType": "text/x-rust" +} diff --git a/packages/core/test/corpus/fixtures/2026-07-28/ResourceListChangedNotification/resources-list-changed.json b/packages/core/test/corpus/fixtures/2026-07-28/ResourceListChangedNotification/resources-list-changed.json new file mode 100644 index 0000000000..6ba5e168ec --- /dev/null +++ b/packages/core/test/corpus/fixtures/2026-07-28/ResourceListChangedNotification/resources-list-changed.json @@ -0,0 +1,4 @@ +{ + "jsonrpc": "2.0", + "method": "notifications/resources/list_changed" +} diff --git a/packages/core/test/corpus/fixtures/2026-07-28/ResourceUpdatedNotification/file-resource-updated-notification.json b/packages/core/test/corpus/fixtures/2026-07-28/ResourceUpdatedNotification/file-resource-updated-notification.json new file mode 100644 index 0000000000..b5f9ef67f7 --- /dev/null +++ b/packages/core/test/corpus/fixtures/2026-07-28/ResourceUpdatedNotification/file-resource-updated-notification.json @@ -0,0 +1,7 @@ +{ + "jsonrpc": "2.0", + "method": "notifications/resources/updated", + "params": { + "uri": "file:///project/src/main.rs" + } +} diff --git a/packages/core/test/corpus/fixtures/2026-07-28/ResourceUpdatedNotificationParams/file-resource-updated.json b/packages/core/test/corpus/fixtures/2026-07-28/ResourceUpdatedNotificationParams/file-resource-updated.json new file mode 100644 index 0000000000..10decf86a2 --- /dev/null +++ b/packages/core/test/corpus/fixtures/2026-07-28/ResourceUpdatedNotificationParams/file-resource-updated.json @@ -0,0 +1,3 @@ +{ + "uri": "file:///project/src/main.rs" +} diff --git a/packages/core/test/corpus/fixtures/2026-07-28/Root/project-directory.json b/packages/core/test/corpus/fixtures/2026-07-28/Root/project-directory.json new file mode 100644 index 0000000000..b3195b3d74 --- /dev/null +++ b/packages/core/test/corpus/fixtures/2026-07-28/Root/project-directory.json @@ -0,0 +1,4 @@ +{ + "uri": "file:///home/user/projects/myproject", + "name": "My Project" +} diff --git a/packages/core/test/corpus/fixtures/2026-07-28/SamplingMessage/multiple-content-blocks.json b/packages/core/test/corpus/fixtures/2026-07-28/SamplingMessage/multiple-content-blocks.json new file mode 100644 index 0000000000..9190b9f16d --- /dev/null +++ b/packages/core/test/corpus/fixtures/2026-07-28/SamplingMessage/multiple-content-blocks.json @@ -0,0 +1,15 @@ +{ + "role": "user", + "content": [ + { + "type": "tool_result", + "toolUseId": "call_123", + "content": [{ "type": "text", "text": "Result 1" }] + }, + { + "type": "tool_result", + "toolUseId": "call_456", + "content": [{ "type": "text", "text": "Result 2" }] + } + ] +} \ No newline at end of file diff --git a/packages/core/test/corpus/fixtures/2026-07-28/SamplingMessage/single-content-block.json b/packages/core/test/corpus/fixtures/2026-07-28/SamplingMessage/single-content-block.json new file mode 100644 index 0000000000..5aaa0f15c3 --- /dev/null +++ b/packages/core/test/corpus/fixtures/2026-07-28/SamplingMessage/single-content-block.json @@ -0,0 +1,7 @@ +{ + "role": "user", + "content": { + "type": "text", + "text": "What is the capital of France?" + } +} \ No newline at end of file diff --git a/packages/core/test/corpus/fixtures/2026-07-28/ServerCapabilities/completions-minimum-baseline-support.json b/packages/core/test/corpus/fixtures/2026-07-28/ServerCapabilities/completions-minimum-baseline-support.json new file mode 100644 index 0000000000..b151d2b774 --- /dev/null +++ b/packages/core/test/corpus/fixtures/2026-07-28/ServerCapabilities/completions-minimum-baseline-support.json @@ -0,0 +1,3 @@ +{ + "completions": {} +} diff --git a/packages/core/test/corpus/fixtures/2026-07-28/ServerCapabilities/extensions-tasks.json b/packages/core/test/corpus/fixtures/2026-07-28/ServerCapabilities/extensions-tasks.json new file mode 100644 index 0000000000..10ed90d38d --- /dev/null +++ b/packages/core/test/corpus/fixtures/2026-07-28/ServerCapabilities/extensions-tasks.json @@ -0,0 +1,5 @@ +{ + "extensions": { + "io.modelcontextprotocol/tasks": {} + } +} diff --git a/packages/core/test/corpus/fixtures/2026-07-28/ServerCapabilities/logging-minimum-baseline-support.json b/packages/core/test/corpus/fixtures/2026-07-28/ServerCapabilities/logging-minimum-baseline-support.json new file mode 100644 index 0000000000..6be7397886 --- /dev/null +++ b/packages/core/test/corpus/fixtures/2026-07-28/ServerCapabilities/logging-minimum-baseline-support.json @@ -0,0 +1,3 @@ +{ + "logging": {} +} diff --git a/packages/core/test/corpus/fixtures/2026-07-28/ServerCapabilities/prompts-list-changed-notifications.json b/packages/core/test/corpus/fixtures/2026-07-28/ServerCapabilities/prompts-list-changed-notifications.json new file mode 100644 index 0000000000..0fcacf6154 --- /dev/null +++ b/packages/core/test/corpus/fixtures/2026-07-28/ServerCapabilities/prompts-list-changed-notifications.json @@ -0,0 +1,5 @@ +{ + "prompts": { + "listChanged": true + } +} diff --git a/packages/core/test/corpus/fixtures/2026-07-28/ServerCapabilities/prompts-minimum-baseline-support.json b/packages/core/test/corpus/fixtures/2026-07-28/ServerCapabilities/prompts-minimum-baseline-support.json new file mode 100644 index 0000000000..03b9366156 --- /dev/null +++ b/packages/core/test/corpus/fixtures/2026-07-28/ServerCapabilities/prompts-minimum-baseline-support.json @@ -0,0 +1,3 @@ +{ + "prompts": {} +} diff --git a/packages/core/test/corpus/fixtures/2026-07-28/ServerCapabilities/resources-all-notifications.json b/packages/core/test/corpus/fixtures/2026-07-28/ServerCapabilities/resources-all-notifications.json new file mode 100644 index 0000000000..52bc7897e9 --- /dev/null +++ b/packages/core/test/corpus/fixtures/2026-07-28/ServerCapabilities/resources-all-notifications.json @@ -0,0 +1,6 @@ +{ + "resources": { + "subscribe": true, + "listChanged": true + } +} diff --git a/packages/core/test/corpus/fixtures/2026-07-28/ServerCapabilities/resources-list-changed-notifications-only.json b/packages/core/test/corpus/fixtures/2026-07-28/ServerCapabilities/resources-list-changed-notifications-only.json new file mode 100644 index 0000000000..0b144588c1 --- /dev/null +++ b/packages/core/test/corpus/fixtures/2026-07-28/ServerCapabilities/resources-list-changed-notifications-only.json @@ -0,0 +1,5 @@ +{ + "resources": { + "listChanged": true + } +} diff --git a/packages/core/test/corpus/fixtures/2026-07-28/ServerCapabilities/resources-minimum-baseline-support.json b/packages/core/test/corpus/fixtures/2026-07-28/ServerCapabilities/resources-minimum-baseline-support.json new file mode 100644 index 0000000000..d6eebc58e8 --- /dev/null +++ b/packages/core/test/corpus/fixtures/2026-07-28/ServerCapabilities/resources-minimum-baseline-support.json @@ -0,0 +1,3 @@ +{ + "resources": {} +} diff --git a/packages/core/test/corpus/fixtures/2026-07-28/ServerCapabilities/resources-subscription-to-individual-resource-updates-only.json b/packages/core/test/corpus/fixtures/2026-07-28/ServerCapabilities/resources-subscription-to-individual-resource-updates-only.json new file mode 100644 index 0000000000..0ec9700ab9 --- /dev/null +++ b/packages/core/test/corpus/fixtures/2026-07-28/ServerCapabilities/resources-subscription-to-individual-resource-updates-only.json @@ -0,0 +1,5 @@ +{ + "resources": { + "subscribe": true + } +} diff --git a/packages/core/test/corpus/fixtures/2026-07-28/ServerCapabilities/tools-list-changed-notifications.json b/packages/core/test/corpus/fixtures/2026-07-28/ServerCapabilities/tools-list-changed-notifications.json new file mode 100644 index 0000000000..73851b6c5c --- /dev/null +++ b/packages/core/test/corpus/fixtures/2026-07-28/ServerCapabilities/tools-list-changed-notifications.json @@ -0,0 +1,5 @@ +{ + "tools": { + "listChanged": true + } +} diff --git a/packages/core/test/corpus/fixtures/2026-07-28/ServerCapabilities/tools-minimum-baseline-support.json b/packages/core/test/corpus/fixtures/2026-07-28/ServerCapabilities/tools-minimum-baseline-support.json new file mode 100644 index 0000000000..2f8e00f819 --- /dev/null +++ b/packages/core/test/corpus/fixtures/2026-07-28/ServerCapabilities/tools-minimum-baseline-support.json @@ -0,0 +1,3 @@ +{ + "tools": {} +} diff --git a/packages/core/test/corpus/fixtures/2026-07-28/StringSchema/email-input-schema.json b/packages/core/test/corpus/fixtures/2026-07-28/StringSchema/email-input-schema.json new file mode 100644 index 0000000000..8d85641332 --- /dev/null +++ b/packages/core/test/corpus/fixtures/2026-07-28/StringSchema/email-input-schema.json @@ -0,0 +1,9 @@ +{ + "type": "string", + "title": "Display Name", + "description": "Description text", + "minLength": 3, + "maxLength": 50, + "format": "email", + "default": "user@example.com" +} diff --git a/packages/core/test/corpus/fixtures/2026-07-28/SubscriptionsAcknowledgedNotification/listen-acknowledged.json b/packages/core/test/corpus/fixtures/2026-07-28/SubscriptionsAcknowledgedNotification/listen-acknowledged.json new file mode 100644 index 0000000000..d3e444f9e6 --- /dev/null +++ b/packages/core/test/corpus/fixtures/2026-07-28/SubscriptionsAcknowledgedNotification/listen-acknowledged.json @@ -0,0 +1,13 @@ +{ + "jsonrpc": "2.0", + "method": "notifications/subscriptions/acknowledged", + "params": { + "_meta": { + "io.modelcontextprotocol/subscriptionId": "listen-1" + }, + "notifications": { + "toolsListChanged": true, + "resourceSubscriptions": ["file:///project/config.json"] + } + } +} diff --git a/packages/core/test/corpus/fixtures/2026-07-28/SubscriptionsListenRequest/listen-for-list-changes.json b/packages/core/test/corpus/fixtures/2026-07-28/SubscriptionsListenRequest/listen-for-list-changes.json new file mode 100644 index 0000000000..76858b497e --- /dev/null +++ b/packages/core/test/corpus/fixtures/2026-07-28/SubscriptionsListenRequest/listen-for-list-changes.json @@ -0,0 +1,19 @@ +{ + "jsonrpc": "2.0", + "id": "listen-1", + "method": "subscriptions/listen", + "params": { + "_meta": { + "io.modelcontextprotocol/protocolVersion": "2026-07-28", + "io.modelcontextprotocol/clientInfo": { + "name": "ExampleClient", + "version": "1.0.0" + }, + "io.modelcontextprotocol/clientCapabilities": {} + }, + "notifications": { + "toolsListChanged": true, + "resourceSubscriptions": ["file:///project/config.json"] + } + } +} diff --git a/packages/core/test/corpus/fixtures/2026-07-28/TextContent/text-content.json b/packages/core/test/corpus/fixtures/2026-07-28/TextContent/text-content.json new file mode 100644 index 0000000000..13df577040 --- /dev/null +++ b/packages/core/test/corpus/fixtures/2026-07-28/TextContent/text-content.json @@ -0,0 +1,4 @@ +{ + "type": "text", + "text": "Tool result text" +} diff --git a/packages/core/test/corpus/fixtures/2026-07-28/TextResourceContents/text-file-contents.json b/packages/core/test/corpus/fixtures/2026-07-28/TextResourceContents/text-file-contents.json new file mode 100644 index 0000000000..a70f268592 --- /dev/null +++ b/packages/core/test/corpus/fixtures/2026-07-28/TextResourceContents/text-file-contents.json @@ -0,0 +1,5 @@ +{ + "uri": "file:///example.txt", + "mimeType": "text/plain", + "text": "Resource content" +} diff --git a/packages/core/test/corpus/fixtures/2026-07-28/TitledMultiSelectEnumSchema/titled-color-multi-select-schema.json b/packages/core/test/corpus/fixtures/2026-07-28/TitledMultiSelectEnumSchema/titled-color-multi-select-schema.json new file mode 100644 index 0000000000..e6b9e6f8a0 --- /dev/null +++ b/packages/core/test/corpus/fixtures/2026-07-28/TitledMultiSelectEnumSchema/titled-color-multi-select-schema.json @@ -0,0 +1,15 @@ +{ + "type": "array", + "title": "Color Selection", + "description": "Choose your favorite colors", + "minItems": 1, + "maxItems": 2, + "items": { + "anyOf": [ + { "const": "#FF0000", "title": "Red" }, + { "const": "#00FF00", "title": "Green" }, + { "const": "#0000FF", "title": "Blue" } + ] + }, + "default": ["#FF0000", "#00FF00"] +} diff --git a/packages/core/test/corpus/fixtures/2026-07-28/TitledSingleSelectEnumSchema/titled-color-select-schema.json b/packages/core/test/corpus/fixtures/2026-07-28/TitledSingleSelectEnumSchema/titled-color-select-schema.json new file mode 100644 index 0000000000..d1a4689195 --- /dev/null +++ b/packages/core/test/corpus/fixtures/2026-07-28/TitledSingleSelectEnumSchema/titled-color-select-schema.json @@ -0,0 +1,11 @@ +{ + "type": "string", + "title": "Color Selection", + "description": "Choose your favorite color", + "oneOf": [ + { "const": "#FF0000", "title": "Red" }, + { "const": "#00FF00", "title": "Green" }, + { "const": "#0000FF", "title": "Blue" } + ], + "default": "#FF0000" +} diff --git a/packages/core/test/corpus/fixtures/2026-07-28/Tool/tool-with-array-output-schema.json b/packages/core/test/corpus/fixtures/2026-07-28/Tool/tool-with-array-output-schema.json new file mode 100644 index 0000000000..8c7edec623 --- /dev/null +++ b/packages/core/test/corpus/fixtures/2026-07-28/Tool/tool-with-array-output-schema.json @@ -0,0 +1,30 @@ +{ + "name": "list_users", + "title": "User List", + "description": "Returns a list of all users", + "inputSchema": { + "type": "object", + "properties": {} + }, + "outputSchema": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "User ID" + }, + "name": { + "type": "string", + "description": "User name" + }, + "email": { + "type": "string", + "description": "User email" + } + }, + "required": ["id", "name", "email"] + } + } +} diff --git a/packages/core/test/corpus/fixtures/2026-07-28/Tool/tool-with-composition-input-schema.json b/packages/core/test/corpus/fixtures/2026-07-28/Tool/tool-with-composition-input-schema.json new file mode 100644 index 0000000000..7c7253af9f --- /dev/null +++ b/packages/core/test/corpus/fixtures/2026-07-28/Tool/tool-with-composition-input-schema.json @@ -0,0 +1,28 @@ +{ + "name": "find_resource", + "title": "Resource Finder", + "description": "Find a resource by ID or name", + "inputSchema": { + "type": "object", + "oneOf": [ + { + "properties": { + "id": { + "type": "string", + "description": "Resource ID" + } + }, + "required": ["id"] + }, + { + "properties": { + "name": { + "type": "string", + "description": "Resource name" + } + }, + "required": ["name"] + } + ] + } +} diff --git a/packages/core/test/corpus/fixtures/2026-07-28/Tool/with-default-2020-12-input-schema.json b/packages/core/test/corpus/fixtures/2026-07-28/Tool/with-default-2020-12-input-schema.json new file mode 100644 index 0000000000..d79a00eeaa --- /dev/null +++ b/packages/core/test/corpus/fixtures/2026-07-28/Tool/with-default-2020-12-input-schema.json @@ -0,0 +1,12 @@ +{ + "name": "calculate_sum", + "description": "Add two numbers", + "inputSchema": { + "type": "object", + "properties": { + "a": { "type": "number" }, + "b": { "type": "number" } + }, + "required": ["a", "b"] + } +} diff --git a/packages/core/test/corpus/fixtures/2026-07-28/Tool/with-explicit-draft-07-input-schema.json b/packages/core/test/corpus/fixtures/2026-07-28/Tool/with-explicit-draft-07-input-schema.json new file mode 100644 index 0000000000..698d95b865 --- /dev/null +++ b/packages/core/test/corpus/fixtures/2026-07-28/Tool/with-explicit-draft-07-input-schema.json @@ -0,0 +1,13 @@ +{ + "name": "calculate_sum", + "description": "Add two numbers", + "inputSchema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "a": { "type": "number" }, + "b": { "type": "number" } + }, + "required": ["a", "b"] + } +} diff --git a/packages/core/test/corpus/fixtures/2026-07-28/Tool/with-no-parameters.json b/packages/core/test/corpus/fixtures/2026-07-28/Tool/with-no-parameters.json new file mode 100644 index 0000000000..04a3a4e956 --- /dev/null +++ b/packages/core/test/corpus/fixtures/2026-07-28/Tool/with-no-parameters.json @@ -0,0 +1,8 @@ +{ + "name": "get_current_time", + "description": "Returns the current server time", + "inputSchema": { + "type": "object", + "additionalProperties": false + } +} diff --git a/packages/core/test/corpus/fixtures/2026-07-28/Tool/with-output-schema-for-structured-content.json b/packages/core/test/corpus/fixtures/2026-07-28/Tool/with-output-schema-for-structured-content.json new file mode 100644 index 0000000000..a146983424 --- /dev/null +++ b/packages/core/test/corpus/fixtures/2026-07-28/Tool/with-output-schema-for-structured-content.json @@ -0,0 +1,33 @@ +{ + "name": "get_weather_data", + "title": "Weather Data Retriever", + "description": "Get current weather data for a location", + "inputSchema": { + "type": "object", + "properties": { + "location": { + "type": "string", + "description": "City name or zip code" + } + }, + "required": ["location"] + }, + "outputSchema": { + "type": "object", + "properties": { + "temperature": { + "type": "number", + "description": "Temperature in celsius" + }, + "conditions": { + "type": "string", + "description": "Weather conditions description" + }, + "humidity": { + "type": "number", + "description": "Humidity percentage" + } + }, + "required": ["temperature", "conditions", "humidity"] + } +} diff --git a/packages/core/test/corpus/fixtures/2026-07-28/ToolListChangedNotification/tools-list-changed.json b/packages/core/test/corpus/fixtures/2026-07-28/ToolListChangedNotification/tools-list-changed.json new file mode 100644 index 0000000000..a28e846763 --- /dev/null +++ b/packages/core/test/corpus/fixtures/2026-07-28/ToolListChangedNotification/tools-list-changed.json @@ -0,0 +1,4 @@ +{ + "jsonrpc": "2.0", + "method": "notifications/tools/list_changed" +} diff --git a/packages/core/test/corpus/fixtures/2026-07-28/ToolResultContent/get-weather-tool-result.json b/packages/core/test/corpus/fixtures/2026-07-28/ToolResultContent/get-weather-tool-result.json new file mode 100644 index 0000000000..3b44156d61 --- /dev/null +++ b/packages/core/test/corpus/fixtures/2026-07-28/ToolResultContent/get-weather-tool-result.json @@ -0,0 +1,10 @@ +{ + "type": "tool_result", + "toolUseId": "call_abc123", + "content": [ + { + "type": "text", + "text": "Weather in Paris: 18°C, partly cloudy" + } + ] +} \ No newline at end of file diff --git a/packages/core/test/corpus/fixtures/2026-07-28/ToolUseContent/get-weather-tool-use.json b/packages/core/test/corpus/fixtures/2026-07-28/ToolUseContent/get-weather-tool-use.json new file mode 100644 index 0000000000..197560de67 --- /dev/null +++ b/packages/core/test/corpus/fixtures/2026-07-28/ToolUseContent/get-weather-tool-use.json @@ -0,0 +1,8 @@ +{ + "type": "tool_use", + "id": "call_abc123", + "name": "get_weather", + "input": { + "city": "Paris" + } +} \ No newline at end of file diff --git a/packages/core/test/corpus/fixtures/2026-07-28/UnsupportedProtocolVersionError/unsupported-version.json b/packages/core/test/corpus/fixtures/2026-07-28/UnsupportedProtocolVersionError/unsupported-version.json new file mode 100644 index 0000000000..d4c99b7ce8 --- /dev/null +++ b/packages/core/test/corpus/fixtures/2026-07-28/UnsupportedProtocolVersionError/unsupported-version.json @@ -0,0 +1,12 @@ +{ + "jsonrpc": "2.0", + "id": 1, + "error": { + "code": -32004, + "message": "Unsupported protocol version", + "data": { + "supported": ["2026-07-28", "2025-11-25"], + "requested": "1900-01-01" + } + } +} diff --git a/packages/core/test/corpus/fixtures/2026-07-28/UntitledMultiSelectEnumSchema/color-multi-select-schema.json b/packages/core/test/corpus/fixtures/2026-07-28/UntitledMultiSelectEnumSchema/color-multi-select-schema.json new file mode 100644 index 0000000000..d63467e7ee --- /dev/null +++ b/packages/core/test/corpus/fixtures/2026-07-28/UntitledMultiSelectEnumSchema/color-multi-select-schema.json @@ -0,0 +1,12 @@ +{ + "type": "array", + "title": "Color Selection", + "description": "Choose your favorite colors", + "minItems": 1, + "maxItems": 2, + "items": { + "type": "string", + "enum": ["Red", "Green", "Blue"] + }, + "default": ["Red", "Green"] +} diff --git a/packages/core/test/corpus/fixtures/2026-07-28/UntitledSingleSelectEnumSchema/color-select-schema.json b/packages/core/test/corpus/fixtures/2026-07-28/UntitledSingleSelectEnumSchema/color-select-schema.json new file mode 100644 index 0000000000..13e05d5789 --- /dev/null +++ b/packages/core/test/corpus/fixtures/2026-07-28/UntitledSingleSelectEnumSchema/color-select-schema.json @@ -0,0 +1,7 @@ +{ + "type": "string", + "title": "Color Selection", + "description": "Choose your favorite color", + "enum": ["Red", "Green", "Blue"], + "default": "Red" +} diff --git a/packages/core/test/corpus/fixtures/2026-07-28/manifest.json b/packages/core/test/corpus/fixtures/2026-07-28/manifest.json new file mode 100644 index 0000000000..8aa8155edd --- /dev/null +++ b/packages/core/test/corpus/fixtures/2026-07-28/manifest.json @@ -0,0 +1,312 @@ +{ + "revision": "2026-07-28", + "source": { + "repo": "modelcontextprotocol/modelcontextprotocol", + "path": "schema/draft/examples", + "commit": "0168c57fc74aba6e6dcf8f0b7191db3caaa5ad65" + }, + "regenerate": "pnpm fetch:spec-examples --spec-dir # or [sha] to fetch from GitHub", + "directoryCount": 86, + "fileCount": 127, + "directories": { + "AudioContent": [ + "audio-wav-content.json" + ], + "BlobResourceContents": [ + "image-file-contents.json" + ], + "BooleanSchema": [ + "boolean-input-schema.json" + ], + "CallToolRequest": [ + "call-tool-request.json" + ], + "CallToolRequestParams": [ + "get-weather-tool-call-params.json", + "tool-call-params-with-progress-token.json" + ], + "CallToolResult": [ + "invalid-tool-input-error.json", + "result-with-array-structured-content.json", + "result-with-structured-content.json", + "result-with-unstructured-text.json" + ], + "CallToolResultResponse": [ + "call-tool-result-response.json" + ], + "CancelledNotification": [ + "user-requested-cancellation.json" + ], + "CancelledNotificationParams": [ + "user-requested-cancellation.json" + ], + "ClientCapabilities": [ + "elicitation-form-and-url-mode-support.json", + "elicitation-form-only-implicit.json", + "extensions-ui-mime-types.json", + "roots-minimum-baseline-support.json", + "sampling-context-inclusion-support-deprecated.json", + "sampling-minimum-baseline-support.json", + "sampling-tool-use-support.json" + ], + "CompleteRequest": [ + "completion-request.json" + ], + "CompleteRequestParams": [ + "prompt-argument-completion-with-context.json", + "prompt-argument-completion.json" + ], + "CompleteResult": [ + "multiple-completion-values-with-more-available.json", + "single-completion-value.json" + ], + "CompleteResultResponse": [ + "completion-result-response.json" + ], + "CreateMessageRequest": [ + "sampling-request.json" + ], + "CreateMessageRequestParams": [ + "basic-request.json", + "follow-up-with-tool-results.json", + "request-with-tools.json" + ], + "CreateMessageResult": [ + "final-response.json", + "text-response.json", + "tool-use-response.json" + ], + "DiscoverRequest": [ + "server-discover-request.json" + ], + "DiscoverResult": [ + "server-capabilities-discovery.json" + ], + "DiscoverResultResponse": [ + "discover-result-response.json" + ], + "ElicitationCompleteNotification": [ + "elicitation-complete.json" + ], + "ElicitRequest": [ + "elicitation-request.json" + ], + "ElicitRequestFormParams": [ + "elicit-multiple-fields.json", + "elicit-single-field.json" + ], + "ElicitRequestURLParams": [ + "elicit-sensitive-data.json" + ], + "ElicitResult": [ + "accept-url-mode-no-content.json", + "input-multiple-fields.json", + "input-single-field.json" + ], + "EmbeddedResource": [ + "embedded-file-resource-with-annotations.json" + ], + "GetPromptRequest": [ + "get-prompt-request.json" + ], + "GetPromptRequestParams": [ + "get-code-review-prompt.json" + ], + "GetPromptResult": [ + "code-review-prompt.json" + ], + "GetPromptResultResponse": [ + "get-prompt-result-response.json" + ], + "ImageContent": [ + "image-png-content-with-annotations.json" + ], + "InputRequests": [ + "elicitation-and-sampling-input-requests.json" + ], + "InputRequiredResult": [ + "input-required-result-with-elicitation-and-sampling-and-request-state.json", + "input-required-result-with-request-state-only.json" + ], + "InputResponses": [ + "elicitation-and-sampling-input-responses.json" + ], + "InternalError": [ + "unexpected-error.json" + ], + "InvalidParamsError": [ + "invalid-cursor.json", + "invalid-tool-arguments.json", + "unknown-prompt.json", + "unknown-tool.json" + ], + "ListPromptsRequest": [ + "list-prompts-request.json" + ], + "ListPromptsResult": [ + "prompts-list-with-cursor-and-ttl.json" + ], + "ListPromptsResultResponse": [ + "list-prompts-result-response.json" + ], + "ListResourcesRequest": [ + "list-resources-request.json" + ], + "ListResourcesResult": [ + "resources-list-with-cursor-and-ttl.json" + ], + "ListResourcesResultResponse": [ + "list-resources-result-response.json" + ], + "ListResourceTemplatesRequest": [ + "list-resource-templates-request.json" + ], + "ListResourceTemplatesResult": [ + "resource-templates-list-with-cursor-and-ttl.json" + ], + "ListResourceTemplatesResultResponse": [ + "list-resource-templates-result-response.json" + ], + "ListRootsRequest": [ + "list-roots-request.json" + ], + "ListRootsResult": [ + "multiple-root-directories.json", + "single-root-directory.json" + ], + "ListToolsRequest": [ + "list-tools-request.json" + ], + "ListToolsResult": [ + "tools-list-with-cursor-and-ttl.json" + ], + "ListToolsResultResponse": [ + "list-tools-result-response.json" + ], + "LoggingMessageNotification": [ + "log-database-connection-failed.json" + ], + "LoggingMessageNotificationParams": [ + "log-database-connection-failed.json" + ], + "MethodNotFoundError": [ + "prompts-not-supported.json" + ], + "MissingRequiredClientCapabilityError": [ + "missing-elicitation-capability.json" + ], + "ModelPreferences": [ + "with-hints-and-priorities.json" + ], + "NumberSchema": [ + "number-input-schema.json" + ], + "PaginatedRequestParams": [ + "list-with-cursor.json" + ], + "ParseError": [ + "invalid-json.json" + ], + "ProgressNotification": [ + "progress-message.json" + ], + "ProgressNotificationParams": [ + "progress-message.json" + ], + "PromptListChangedNotification": [ + "prompts-list-changed.json" + ], + "ReadResourceRequest": [ + "read-resource-request.json" + ], + "ReadResourceResult": [ + "file-resource-contents.json" + ], + "ReadResourceResultResponse": [ + "read-resource-result-response-with-ttl.json", + "read-resource-result-response.json" + ], + "Resource": [ + "file-resource-with-annotations.json" + ], + "ResourceLink": [ + "file-resource-link.json" + ], + "ResourceListChangedNotification": [ + "resources-list-changed.json" + ], + "ResourceUpdatedNotification": [ + "file-resource-updated-notification.json" + ], + "ResourceUpdatedNotificationParams": [ + "file-resource-updated.json" + ], + "Root": [ + "project-directory.json" + ], + "SamplingMessage": [ + "multiple-content-blocks.json", + "single-content-block.json" + ], + "ServerCapabilities": [ + "completions-minimum-baseline-support.json", + "extensions-tasks.json", + "logging-minimum-baseline-support.json", + "prompts-list-changed-notifications.json", + "prompts-minimum-baseline-support.json", + "resources-all-notifications.json", + "resources-list-changed-notifications-only.json", + "resources-minimum-baseline-support.json", + "resources-subscription-to-individual-resource-updates-only.json", + "tools-list-changed-notifications.json", + "tools-minimum-baseline-support.json" + ], + "StringSchema": [ + "email-input-schema.json" + ], + "SubscriptionsAcknowledgedNotification": [ + "listen-acknowledged.json" + ], + "SubscriptionsListenRequest": [ + "listen-for-list-changes.json" + ], + "TextContent": [ + "text-content.json" + ], + "TextResourceContents": [ + "text-file-contents.json" + ], + "TitledMultiSelectEnumSchema": [ + "titled-color-multi-select-schema.json" + ], + "TitledSingleSelectEnumSchema": [ + "titled-color-select-schema.json" + ], + "Tool": [ + "tool-with-array-output-schema.json", + "tool-with-composition-input-schema.json", + "with-default-2020-12-input-schema.json", + "with-explicit-draft-07-input-schema.json", + "with-no-parameters.json", + "with-output-schema-for-structured-content.json" + ], + "ToolListChangedNotification": [ + "tools-list-changed.json" + ], + "ToolResultContent": [ + "get-weather-tool-result.json" + ], + "ToolUseContent": [ + "get-weather-tool-use.json" + ], + "UnsupportedProtocolVersionError": [ + "unsupported-version.json" + ], + "UntitledMultiSelectEnumSchema": [ + "color-multi-select-schema.json" + ], + "UntitledSingleSelectEnumSchema": [ + "color-select-schema.json" + ] + } +} diff --git a/packages/core/test/corpus/fixtures/rejection/batch-array-body.json b/packages/core/test/corpus/fixtures/rejection/batch-array-body.json new file mode 100644 index 0000000000..bfea09f9a4 --- /dev/null +++ b/packages/core/test/corpus/fixtures/rejection/batch-array-body.json @@ -0,0 +1,11 @@ +{ + "description": "JSON-RPC batch arrays were removed in 2025-06-18; an array message is rejected at classification.", + "message": [ + { + "jsonrpc": "2.0", + "id": 6, + "method": "ping" + } + ], + "expect": "onerror" +} diff --git a/packages/core/test/corpus/fixtures/rejection/error-response-unknown-id.json b/packages/core/test/corpus/fixtures/rejection/error-response-unknown-id.json new file mode 100644 index 0000000000..97c74928ac --- /dev/null +++ b/packages/core/test/corpus/fixtures/rejection/error-response-unknown-id.json @@ -0,0 +1,12 @@ +{ + "description": "An error response whose id matches no in-flight request is reported out-of-band.", + "message": { + "jsonrpc": "2.0", + "id": 98, + "error": { + "code": -32603, + "message": "boom" + } + }, + "expect": "onerror" +} diff --git a/packages/core/test/corpus/fixtures/rejection/invalid-spec-params.json b/packages/core/test/corpus/fixtures/rejection/invalid-spec-params.json new file mode 100644 index 0000000000..5bf8c693f3 --- /dev/null +++ b/packages/core/test/corpus/fixtures/rejection/invalid-spec-params.json @@ -0,0 +1,13 @@ +{ + "description": "A spec request with params that fail the method schema is answered with an error response (current dispatch surfaces the parse failure as -32603 Internal error).", + "message": { + "jsonrpc": "2.0", + "id": 3, + "method": "tools/call", + "params": { + "name": 123 + } + }, + "expect": "error-response", + "errorCode": -32603 +} diff --git a/packages/core/test/corpus/fixtures/rejection/notification-invalid-spec-params.json b/packages/core/test/corpus/fixtures/rejection/notification-invalid-spec-params.json new file mode 100644 index 0000000000..7ea984842e --- /dev/null +++ b/packages/core/test/corpus/fixtures/rejection/notification-invalid-spec-params.json @@ -0,0 +1,11 @@ +{ + "description": "A spec notification whose params fail the method schema is dropped; the failure is reported out-of-band and no response is sent.", + "message": { + "jsonrpc": "2.0", + "method": "notifications/cancelled", + "params": { + "requestId": true + } + }, + "expect": "onerror" +} diff --git a/packages/core/test/corpus/fixtures/rejection/notification-unknown-method.json b/packages/core/test/corpus/fixtures/rejection/notification-unknown-method.json new file mode 100644 index 0000000000..2409ad03c8 --- /dev/null +++ b/packages/core/test/corpus/fixtures/rejection/notification-unknown-method.json @@ -0,0 +1,8 @@ +{ + "description": "A notification with no registered handler is silently ignored (no response, no out-of-band error).", + "message": { + "jsonrpc": "2.0", + "method": "notifications/definitely-unknown" + }, + "expect": "ignored" +} diff --git a/packages/core/test/corpus/fixtures/rejection/null-request-id.json b/packages/core/test/corpus/fixtures/rejection/null-request-id.json new file mode 100644 index 0000000000..5517f83f3b --- /dev/null +++ b/packages/core/test/corpus/fixtures/rejection/null-request-id.json @@ -0,0 +1,9 @@ +{ + "description": "A request id of null is invalid (ids are strings or integers); the message is rejected at classification.", + "message": { + "jsonrpc": "2.0", + "id": null, + "method": "ping" + }, + "expect": "onerror" +} diff --git a/packages/core/test/corpus/fixtures/rejection/request-extra-top-level-key.json b/packages/core/test/corpus/fixtures/rejection/request-extra-top-level-key.json new file mode 100644 index 0000000000..ef0178a1c3 --- /dev/null +++ b/packages/core/test/corpus/fixtures/rejection/request-extra-top-level-key.json @@ -0,0 +1,11 @@ +{ + "description": "A request envelope with an unknown top-level sibling is not a valid JSON-RPC message; dispatch reports it out-of-band and sends no response.", + "message": { + "jsonrpc": "2.0", + "id": 4, + "method": "ping", + "params": {}, + "extraTop": true + }, + "expect": "onerror" +} diff --git a/packages/core/test/corpus/fixtures/rejection/result-not-an-object.json b/packages/core/test/corpus/fixtures/rejection/result-not-an-object.json new file mode 100644 index 0000000000..6d8018b445 --- /dev/null +++ b/packages/core/test/corpus/fixtures/rejection/result-not-an-object.json @@ -0,0 +1,9 @@ +{ + "description": "A response whose result member is not an object fails envelope classification.", + "message": { + "jsonrpc": "2.0", + "id": 7, + "result": "nope" + }, + "expect": "onerror" +} diff --git a/packages/core/test/corpus/fixtures/rejection/result-response-unknown-id.json b/packages/core/test/corpus/fixtures/rejection/result-response-unknown-id.json new file mode 100644 index 0000000000..1538b29058 --- /dev/null +++ b/packages/core/test/corpus/fixtures/rejection/result-response-unknown-id.json @@ -0,0 +1,9 @@ +{ + "description": "A result response whose id matches no in-flight request is reported out-of-band.", + "message": { + "jsonrpc": "2.0", + "id": 99, + "result": {} + }, + "expect": "onerror" +} diff --git a/packages/core/test/corpus/fixtures/rejection/unknown-request-method.json b/packages/core/test/corpus/fixtures/rejection/unknown-request-method.json new file mode 100644 index 0000000000..bd5727183f --- /dev/null +++ b/packages/core/test/corpus/fixtures/rejection/unknown-request-method.json @@ -0,0 +1,11 @@ +{ + "description": "A request whose method is unknown to the receiver is answered with -32601 Method not found.", + "message": { + "jsonrpc": "2.0", + "id": 1, + "method": "vendor/definitely-unknown", + "params": {} + }, + "expect": "error-response", + "errorCode": -32601 +} diff --git a/packages/core/test/corpus/fixtures/rejection/unregistered-spec-method.json b/packages/core/test/corpus/fixtures/rejection/unregistered-spec-method.json new file mode 100644 index 0000000000..f7b8d91062 --- /dev/null +++ b/packages/core/test/corpus/fixtures/rejection/unregistered-spec-method.json @@ -0,0 +1,13 @@ +{ + "description": "A spec request method with no registered handler is answered with -32601 (handler absence, not schema absence).", + "message": { + "jsonrpc": "2.0", + "id": 2, + "method": "resources/subscribe", + "params": { + "uri": "file:///a.txt" + } + }, + "expect": "error-response", + "errorCode": -32601 +} diff --git a/packages/core/test/corpus/fixtures/rejection/valid-tools-call.json b/packages/core/test/corpus/fixtures/rejection/valid-tools-call.json new file mode 100644 index 0000000000..f25225d874 --- /dev/null +++ b/packages/core/test/corpus/fixtures/rejection/valid-tools-call.json @@ -0,0 +1,15 @@ +{ + "description": "Accept-side dispatch sanity: a valid tools/call request reaches the registered handler and produces a result response.", + "message": { + "jsonrpc": "2.0", + "id": 8, + "method": "tools/call", + "params": { + "name": "echo", + "arguments": { + "text": "hi" + } + } + }, + "expect": "result-response" +} diff --git a/packages/core/test/corpus/fixtures/rejection/wrong-jsonrpc-version.json b/packages/core/test/corpus/fixtures/rejection/wrong-jsonrpc-version.json new file mode 100644 index 0000000000..04d27bf6e4 --- /dev/null +++ b/packages/core/test/corpus/fixtures/rejection/wrong-jsonrpc-version.json @@ -0,0 +1,9 @@ +{ + "description": "A message with a jsonrpc member other than '2.0' is not a valid JSON-RPC message; dispatch reports it out-of-band and sends no response.", + "message": { + "jsonrpc": "1.0", + "id": 5, + "method": "ping" + }, + "expect": "onerror" +} diff --git a/packages/core/test/corpus/schema-twins/2025-11-25.schema.json b/packages/core/test/corpus/schema-twins/2025-11-25.schema.json new file mode 100644 index 0000000000..9d2e662a26 --- /dev/null +++ b/packages/core/test/corpus/schema-twins/2025-11-25.schema.json @@ -0,0 +1,4058 @@ +{ + "$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": { + "additionalProperties": {}, + "description": "See [General fields: `_meta`](/specification/2025-11-25/basic/index#meta) for notes on `_meta` usage.", + "type": "object" + }, + "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 Tool,\nwhere `annotations.title` should be given precedence over using `name`,\nif present).", + "type": "string" + } + }, + "required": [ + "name" + ], + "type": "object" + }, + "BlobResourceContents": { + "properties": { + "_meta": { + "additionalProperties": {}, + "description": "See [General fields: `_meta`](/specification/2025-11-25/basic/index#meta) for notes on `_meta` usage.", + "type": "object" + }, + "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" + }, + "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": { + "additionalProperties": {}, + "description": "See [General fields: `_meta`](/specification/2025-11-25/basic/index#meta) for notes on `_meta` usage.", + "properties": { + "progressToken": { + "$ref": "#/$defs/ProgressToken", + "description": "If specified, the caller is requesting out-of-band progress notifications for this request (as represented by 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." + } + }, + "type": "object" + }, + "arguments": { + "additionalProperties": {}, + "description": "Arguments to use for the tool call.", + "type": "object" + }, + "name": { + "description": "The name of the tool.", + "type": "string" + }, + "task": { + "$ref": "#/$defs/TaskMetadata", + "description": "If specified, the caller is requesting task-augmented execution for this request.\nThe request will return a CreateTaskResult immediately, and the actual result can be\nretrieved later via tasks/result.\n\nTask augmentation is subject to capability negotiation - receivers MUST declare support\nfor task augmentation of specific request types in their capabilities." + } + }, + "required": [ + "name" + ], + "type": "object" + }, + "CallToolResult": { + "description": "The server's response to a tool call.", + "properties": { + "_meta": { + "additionalProperties": {}, + "description": "See [General fields: `_meta`](/specification/2025-11-25/basic/index#meta) for notes on `_meta` usage.", + "type": "object" + }, + "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" + }, + "structuredContent": { + "additionalProperties": {}, + "description": "An optional JSON object that represents the structured result of the tool call.", + "type": "object" + } + }, + "required": [ + "content" + ], + "type": "object" + }, + "CancelTaskRequest": { + "description": "A request to cancel a task.", + "properties": { + "id": { + "$ref": "#/$defs/RequestId" + }, + "jsonrpc": { + "const": "2.0", + "type": "string" + }, + "method": { + "const": "tasks/cancel", + "type": "string" + }, + "params": { + "properties": { + "taskId": { + "description": "The task identifier to cancel.", + "type": "string" + } + }, + "required": [ + "taskId" + ], + "type": "object" + } + }, + "required": [ + "id", + "jsonrpc", + "method", + "params" + ], + "type": "object" + }, + "CancelTaskResult": { + "allOf": [ + { + "$ref": "#/$defs/Result" + }, + { + "$ref": "#/$defs/Task" + } + ], + "description": "The response to a tasks/cancel request." + }, + "CancelledNotification": { + "description": "This notification can be sent by either side to indicate that it is cancelling a previously-issued 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.\n\nA client MUST NOT attempt to cancel its `initialize` request.\n\nFor task cancellation, use the `tasks/cancel` request instead of this notification.", + "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": { + "additionalProperties": {}, + "description": "See [General fields: `_meta`](/specification/2025-11-25/basic/index#meta) for notes on `_meta` usage.", + "type": "object" + }, + "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 previously issued in the same direction.\nThis MUST be provided for cancelling non-task requests.\nThis MUST NOT be used for cancelling tasks (use the `tasks/cancel` request instead)." + } + }, + "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": { + "additionalProperties": true, + "properties": {}, + "type": "object" + }, + "url": { + "additionalProperties": true, + "properties": {}, + "type": "object" + } + }, + "type": "object" + }, + "experimental": { + "additionalProperties": { + "additionalProperties": true, + "properties": {}, + "type": "object" + }, + "description": "Experimental, non-standard capabilities that the client supports.", + "type": "object" + }, + "roots": { + "description": "Present if the client supports listing roots.", + "properties": { + "listChanged": { + "description": "Whether the client supports notifications for changes to the roots list.", + "type": "boolean" + } + }, + "type": "object" + }, + "sampling": { + "description": "Present if the client supports sampling from an LLM.", + "properties": { + "context": { + "additionalProperties": true, + "description": "Whether the client supports context inclusion via includeContext parameter.\nIf not declared, servers SHOULD only use `includeContext: \"none\"` (or omit it).", + "properties": {}, + "type": "object" + }, + "tools": { + "additionalProperties": true, + "description": "Whether the client supports tool use via tools and toolChoice parameters.", + "properties": {}, + "type": "object" + } + }, + "type": "object" + }, + "tasks": { + "description": "Present if the client supports task-augmented requests.", + "properties": { + "cancel": { + "additionalProperties": true, + "description": "Whether this client supports tasks/cancel.", + "properties": {}, + "type": "object" + }, + "list": { + "additionalProperties": true, + "description": "Whether this client supports tasks/list.", + "properties": {}, + "type": "object" + }, + "requests": { + "description": "Specifies which request types can be augmented with tasks.", + "properties": { + "elicitation": { + "description": "Task support for elicitation-related requests.", + "properties": { + "create": { + "additionalProperties": true, + "description": "Whether the client supports task-augmented elicitation/create requests.", + "properties": {}, + "type": "object" + } + }, + "type": "object" + }, + "sampling": { + "description": "Task support for sampling-related requests.", + "properties": { + "createMessage": { + "additionalProperties": true, + "description": "Whether the client supports task-augmented sampling/createMessage requests.", + "properties": {}, + "type": "object" + } + }, + "type": "object" + } + }, + "type": "object" + } + }, + "type": "object" + } + }, + "type": "object" + }, + "ClientNotification": { + "anyOf": [ + { + "$ref": "#/$defs/CancelledNotification" + }, + { + "$ref": "#/$defs/InitializedNotification" + }, + { + "$ref": "#/$defs/ProgressNotification" + }, + { + "$ref": "#/$defs/TaskStatusNotification" + }, + { + "$ref": "#/$defs/RootsListChangedNotification" + } + ] + }, + "ClientRequest": { + "anyOf": [ + { + "$ref": "#/$defs/InitializeRequest" + }, + { + "$ref": "#/$defs/PingRequest" + }, + { + "$ref": "#/$defs/ListResourcesRequest" + }, + { + "$ref": "#/$defs/ListResourceTemplatesRequest" + }, + { + "$ref": "#/$defs/ReadResourceRequest" + }, + { + "$ref": "#/$defs/SubscribeRequest" + }, + { + "$ref": "#/$defs/UnsubscribeRequest" + }, + { + "$ref": "#/$defs/ListPromptsRequest" + }, + { + "$ref": "#/$defs/GetPromptRequest" + }, + { + "$ref": "#/$defs/ListToolsRequest" + }, + { + "$ref": "#/$defs/CallToolRequest" + }, + { + "$ref": "#/$defs/GetTaskRequest" + }, + { + "$ref": "#/$defs/GetTaskPayloadRequest" + }, + { + "$ref": "#/$defs/CancelTaskRequest" + }, + { + "$ref": "#/$defs/ListTasksRequest" + }, + { + "$ref": "#/$defs/SetLevelRequest" + }, + { + "$ref": "#/$defs/CompleteRequest" + } + ] + }, + "ClientResult": { + "anyOf": [ + { + "$ref": "#/$defs/Result" + }, + { + "$ref": "#/$defs/GetTaskResult", + "description": "The response to a tasks/get request." + }, + { + "$ref": "#/$defs/GetTaskPayloadResult" + }, + { + "$ref": "#/$defs/CancelTaskResult", + "description": "The response to a tasks/cancel request." + }, + { + "$ref": "#/$defs/ListTasksResult" + }, + { + "$ref": "#/$defs/CreateMessageResult" + }, + { + "$ref": "#/$defs/ListRootsResult" + }, + { + "$ref": "#/$defs/ElicitResult" + } + ] + }, + "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": { + "additionalProperties": {}, + "description": "See [General fields: `_meta`](/specification/2025-11-25/basic/index#meta) for notes on `_meta` usage.", + "properties": { + "progressToken": { + "$ref": "#/$defs/ProgressToken", + "description": "If specified, the caller is requesting out-of-band progress notifications for this request (as represented by 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." + } + }, + "type": "object" + }, + "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": [ + "argument", + "ref" + ], + "type": "object" + }, + "CompleteResult": { + "description": "The server's response to a completion/complete request", + "properties": { + "_meta": { + "additionalProperties": {}, + "description": "See [General fields: `_meta`](/specification/2025-11-25/basic/index#meta) for notes on `_meta` usage.", + "type": "object" + }, + "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" + }, + "type": "array" + } + }, + "required": [ + "values" + ], + "type": "object" + } + }, + "required": [ + "completion" + ], + "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": { + "id": { + "$ref": "#/$defs/RequestId" + }, + "jsonrpc": { + "const": "2.0", + "type": "string" + }, + "method": { + "const": "sampling/createMessage", + "type": "string" + }, + "params": { + "$ref": "#/$defs/CreateMessageRequestParams" + } + }, + "required": [ + "id", + "jsonrpc", + "method", + "params" + ], + "type": "object" + }, + "CreateMessageRequestParams": { + "description": "Parameters for a `sampling/createMessage` request.", + "properties": { + "_meta": { + "additionalProperties": {}, + "description": "See [General fields: `_meta`](/specification/2025-11-25/basic/index#meta) for notes on `_meta` usage.", + "properties": { + "progressToken": { + "$ref": "#/$defs/ProgressToken", + "description": "If specified, the caller is requesting out-of-band progress notifications for this request (as represented by 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." + } + }, + "type": "object" + }, + "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\". Values \"thisServer\" and \"allServers\" are soft-deprecated. Servers SHOULD only use these values if the client\ndeclares ClientCapabilities.sampling.context. These values may be removed in future spec releases.", + "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": { + "additionalProperties": true, + "description": "Optional metadata to pass through to the LLM provider. The format of this metadata is provider-specific.", + "properties": {}, + "type": "object" + }, + "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" + }, + "task": { + "$ref": "#/$defs/TaskMetadata", + "description": "If specified, the caller is requesting task-augmented execution for this request.\nThe request will return a CreateTaskResult immediately, and the actual result can be\nretrieved later via tasks/result.\n\nTask augmentation is subject to capability negotiation - receivers MUST declare support\nfor task augmentation of specific request types in their capabilities." + }, + "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 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 ClientCapabilities.sampling.tools is not declared.", + "items": { + "$ref": "#/$defs/Tool" + }, + "type": "array" + } + }, + "required": [ + "maxTokens", + "messages" + ], + "type": "object" + }, + "CreateMessageResult": { + "description": "The client's response to a sampling/createMessage request from the server.\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": { + "additionalProperties": {}, + "description": "See [General fields: `_meta`](/specification/2025-11-25/basic/index#meta) for notes on `_meta` usage.", + "type": "object" + }, + "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" + }, + "CreateTaskResult": { + "description": "A response to a task-augmented request.", + "properties": { + "_meta": { + "additionalProperties": {}, + "description": "See [General fields: `_meta`](/specification/2025-11-25/basic/index#meta) for notes on `_meta` usage.", + "type": "object" + }, + "task": { + "$ref": "#/$defs/Task" + } + }, + "required": [ + "task" + ], + "type": "object" + }, + "Cursor": { + "description": "An opaque token used to represent a cursor for pagination.", + "type": "string" + }, + "ElicitRequest": { + "description": "A request from the server to elicit additional information from the user via the client.", + "properties": { + "id": { + "$ref": "#/$defs/RequestId" + }, + "jsonrpc": { + "const": "2.0", + "type": "string" + }, + "method": { + "const": "elicitation/create", + "type": "string" + }, + "params": { + "$ref": "#/$defs/ElicitRequestParams" + } + }, + "required": [ + "id", + "jsonrpc", + "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": { + "_meta": { + "additionalProperties": {}, + "description": "See [General fields: `_meta`](/specification/2025-11-25/basic/index#meta) for notes on `_meta` usage.", + "properties": { + "progressToken": { + "$ref": "#/$defs/ProgressToken", + "description": "If specified, the caller is requesting out-of-band progress notifications for this request (as represented by 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." + } + }, + "type": "object" + }, + "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" + }, + "task": { + "$ref": "#/$defs/TaskMetadata", + "description": "If specified, the caller is requesting task-augmented execution for this request.\nThe request will return a CreateTaskResult immediately, and the actual result can be\nretrieved later via tasks/result.\n\nTask augmentation is subject to capability negotiation - receivers MUST declare support\nfor task augmentation of specific request types in their capabilities." + } + }, + "required": [ + "message", + "requestedSchema" + ], + "type": "object" + }, + "ElicitRequestParams": { + "anyOf": [ + { + "$ref": "#/$defs/ElicitRequestURLParams" + }, + { + "$ref": "#/$defs/ElicitRequestFormParams" + } + ], + "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": { + "_meta": { + "additionalProperties": {}, + "description": "See [General fields: `_meta`](/specification/2025-11-25/basic/index#meta) for notes on `_meta` usage.", + "properties": { + "progressToken": { + "$ref": "#/$defs/ProgressToken", + "description": "If specified, the caller is requesting out-of-band progress notifications for this request (as represented by 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." + } + }, + "type": "object" + }, + "elicitationId": { + "description": "The ID of the elicitation, which must be unique within the context of the server.\nThe client MUST treat this ID as an opaque value.", + "type": "string" + }, + "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" + }, + "task": { + "$ref": "#/$defs/TaskMetadata", + "description": "If specified, the caller is requesting task-augmented execution for this request.\nThe request will return a CreateTaskResult immediately, and the actual result can be\nretrieved later via tasks/result.\n\nTask augmentation is subject to capability negotiation - receivers MUST declare support\nfor task augmentation of specific request types in their capabilities." + }, + "url": { + "description": "The URL that the user should navigate to.", + "format": "uri", + "type": "string" + } + }, + "required": [ + "elicitationId", + "message", + "mode", + "url" + ], + "type": "object" + }, + "ElicitResult": { + "description": "The client's response to an elicitation request.", + "properties": { + "_meta": { + "additionalProperties": {}, + "description": "See [General fields: `_meta`](/specification/2025-11-25/basic/index#meta) for notes on `_meta` usage.", + "type": "object" + }, + "action": { + "description": "The user action in response to the elicitation.\n- \"accept\": User submitted the form/confirmed the action\n- \"decline\": User explicitly decline 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" + }, + "ElicitationCompleteNotification": { + "description": "An optional notification from the server to the client, informing it of a completion of a out-of-band elicitation request.", + "properties": { + "jsonrpc": { + "const": "2.0", + "type": "string" + }, + "method": { + "const": "notifications/elicitation/complete", + "type": "string" + }, + "params": { + "properties": { + "elicitationId": { + "description": "The ID of the elicitation that completed.", + "type": "string" + } + }, + "required": [ + "elicitationId" + ], + "type": "object" + } + }, + "required": [ + "jsonrpc", + "method", + "params" + ], + "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": { + "additionalProperties": {}, + "description": "See [General fields: `_meta`](/specification/2025-11-25/basic/index#meta) for notes on `_meta` usage.", + "type": "object" + }, + "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" + }, + "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": { + "additionalProperties": {}, + "description": "See [General fields: `_meta`](/specification/2025-11-25/basic/index#meta) for notes on `_meta` usage.", + "properties": { + "progressToken": { + "$ref": "#/$defs/ProgressToken", + "description": "If specified, the caller is requesting out-of-band progress notifications for this request (as represented by 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." + } + }, + "type": "object" + }, + "arguments": { + "additionalProperties": { + "type": "string" + }, + "description": "Arguments to use for templating the prompt.", + "type": "object" + }, + "name": { + "description": "The name of the prompt or prompt template.", + "type": "string" + } + }, + "required": [ + "name" + ], + "type": "object" + }, + "GetPromptResult": { + "description": "The server's response to a prompts/get request from the client.", + "properties": { + "_meta": { + "additionalProperties": {}, + "description": "See [General fields: `_meta`](/specification/2025-11-25/basic/index#meta) for notes on `_meta` usage.", + "type": "object" + }, + "description": { + "description": "An optional description for the prompt.", + "type": "string" + }, + "messages": { + "items": { + "$ref": "#/$defs/PromptMessage" + }, + "type": "array" + } + }, + "required": [ + "messages" + ], + "type": "object" + }, + "GetTaskPayloadRequest": { + "description": "A request to retrieve the result of a completed task.", + "properties": { + "id": { + "$ref": "#/$defs/RequestId" + }, + "jsonrpc": { + "const": "2.0", + "type": "string" + }, + "method": { + "const": "tasks/result", + "type": "string" + }, + "params": { + "properties": { + "taskId": { + "description": "The task identifier to retrieve results for.", + "type": "string" + } + }, + "required": [ + "taskId" + ], + "type": "object" + } + }, + "required": [ + "id", + "jsonrpc", + "method", + "params" + ], + "type": "object" + }, + "GetTaskPayloadResult": { + "additionalProperties": {}, + "description": "The response to a tasks/result request.\nThe structure matches the result type of the original request.\nFor example, a tools/call task would return the CallToolResult structure.", + "properties": { + "_meta": { + "additionalProperties": {}, + "description": "See [General fields: `_meta`](/specification/2025-11-25/basic/index#meta) for notes on `_meta` usage.", + "type": "object" + } + }, + "type": "object" + }, + "GetTaskRequest": { + "description": "A request to retrieve the state of a task.", + "properties": { + "id": { + "$ref": "#/$defs/RequestId" + }, + "jsonrpc": { + "const": "2.0", + "type": "string" + }, + "method": { + "const": "tasks/get", + "type": "string" + }, + "params": { + "properties": { + "taskId": { + "description": "The task identifier to query.", + "type": "string" + } + }, + "required": [ + "taskId" + ], + "type": "object" + } + }, + "required": [ + "id", + "jsonrpc", + "method", + "params" + ], + "type": "object" + }, + "GetTaskResult": { + "allOf": [ + { + "$ref": "#/$defs/Result" + }, + { + "$ref": "#/$defs/Task" + } + ], + "description": "The response to a tasks/get request." + }, + "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 takes 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": { + "additionalProperties": {}, + "description": "See [General fields: `_meta`](/specification/2025-11-25/basic/index#meta) for notes on `_meta` usage.", + "type": "object" + }, + "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 Tool,\nwhere `annotations.title` should be given precedence over using `name`,\nif present).", + "type": "string" + }, + "version": { + "type": "string" + }, + "websiteUrl": { + "description": "An optional URL of the website for this implementation.", + "format": "uri", + "type": "string" + } + }, + "required": [ + "name", + "version" + ], + "type": "object" + }, + "InitializeRequest": { + "description": "This request is sent from the client to the server when it first connects, asking it to begin initialization.", + "properties": { + "id": { + "$ref": "#/$defs/RequestId" + }, + "jsonrpc": { + "const": "2.0", + "type": "string" + }, + "method": { + "const": "initialize", + "type": "string" + }, + "params": { + "$ref": "#/$defs/InitializeRequestParams" + } + }, + "required": [ + "id", + "jsonrpc", + "method", + "params" + ], + "type": "object" + }, + "InitializeRequestParams": { + "description": "Parameters for an `initialize` request.", + "properties": { + "_meta": { + "additionalProperties": {}, + "description": "See [General fields: `_meta`](/specification/2025-11-25/basic/index#meta) for notes on `_meta` usage.", + "properties": { + "progressToken": { + "$ref": "#/$defs/ProgressToken", + "description": "If specified, the caller is requesting out-of-band progress notifications for this request (as represented by 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." + } + }, + "type": "object" + }, + "capabilities": { + "$ref": "#/$defs/ClientCapabilities" + }, + "clientInfo": { + "$ref": "#/$defs/Implementation" + }, + "protocolVersion": { + "description": "The latest version of the Model Context Protocol that the client supports. The client MAY decide to support older versions as well.", + "type": "string" + } + }, + "required": [ + "capabilities", + "clientInfo", + "protocolVersion" + ], + "type": "object" + }, + "InitializeResult": { + "description": "After receiving an initialize request from the client, the server sends this response.", + "properties": { + "_meta": { + "additionalProperties": {}, + "description": "See [General fields: `_meta`](/specification/2025-11-25/basic/index#meta) for notes on `_meta` usage.", + "type": "object" + }, + "capabilities": { + "$ref": "#/$defs/ServerCapabilities" + }, + "instructions": { + "description": "Instructions describing how to use the server and its features.\n\nThis can be used by clients to improve the LLM's understanding of available tools, resources, etc. It can be thought of like a \"hint\" to the model. For example, this information MAY be added to the system prompt.", + "type": "string" + }, + "protocolVersion": { + "description": "The version of the Model Context Protocol that the server wants to use. This may not match the version that the client requested. If the client cannot support this version, it MUST disconnect.", + "type": "string" + }, + "serverInfo": { + "$ref": "#/$defs/Implementation" + } + }, + "required": [ + "capabilities", + "protocolVersion", + "serverInfo" + ], + "type": "object" + }, + "InitializedNotification": { + "description": "This notification is sent from the client to the server after initialization has finished.", + "properties": { + "jsonrpc": { + "const": "2.0", + "type": "string" + }, + "method": { + "const": "notifications/initialized", + "type": "string" + }, + "params": { + "$ref": "#/$defs/NotificationParams" + } + }, + "required": [ + "jsonrpc", + "method" + ], + "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" + }, + "LegacyTitledEnumSchema": { + "description": "Use 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" + ], + "type": "object" + }, + "ListPromptsResult": { + "description": "The server's response to a prompts/list request from the client.", + "properties": { + "_meta": { + "additionalProperties": {}, + "description": "See [General fields: `_meta`](/specification/2025-11-25/basic/index#meta) for notes on `_meta` usage.", + "type": "object" + }, + "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" + } + }, + "required": [ + "prompts" + ], + "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" + ], + "type": "object" + }, + "ListResourceTemplatesResult": { + "description": "The server's response to a resources/templates/list request from the client.", + "properties": { + "_meta": { + "additionalProperties": {}, + "description": "See [General fields: `_meta`](/specification/2025-11-25/basic/index#meta) for notes on `_meta` usage.", + "type": "object" + }, + "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" + } + }, + "required": [ + "resourceTemplates" + ], + "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" + ], + "type": "object" + }, + "ListResourcesResult": { + "description": "The server's response to a resources/list request from the client.", + "properties": { + "_meta": { + "additionalProperties": {}, + "description": "See [General fields: `_meta`](/specification/2025-11-25/basic/index#meta) for notes on `_meta` usage.", + "type": "object" + }, + "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" + } + }, + "required": [ + "resources" + ], + "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": { + "id": { + "$ref": "#/$defs/RequestId" + }, + "jsonrpc": { + "const": "2.0", + "type": "string" + }, + "method": { + "const": "roots/list", + "type": "string" + }, + "params": { + "$ref": "#/$defs/RequestParams" + } + }, + "required": [ + "id", + "jsonrpc", + "method" + ], + "type": "object" + }, + "ListRootsResult": { + "description": "The client's response to a roots/list request from the server.\nThis result contains an array of Root objects, each representing a root directory\nor file that the server can operate on.", + "properties": { + "_meta": { + "additionalProperties": {}, + "description": "See [General fields: `_meta`](/specification/2025-11-25/basic/index#meta) for notes on `_meta` usage.", + "type": "object" + }, + "roots": { + "items": { + "$ref": "#/$defs/Root" + }, + "type": "array" + } + }, + "required": [ + "roots" + ], + "type": "object" + }, + "ListTasksRequest": { + "description": "A request to retrieve a list of tasks.", + "properties": { + "id": { + "$ref": "#/$defs/RequestId" + }, + "jsonrpc": { + "const": "2.0", + "type": "string" + }, + "method": { + "const": "tasks/list", + "type": "string" + }, + "params": { + "$ref": "#/$defs/PaginatedRequestParams" + } + }, + "required": [ + "id", + "jsonrpc", + "method" + ], + "type": "object" + }, + "ListTasksResult": { + "description": "The response to a tasks/list request.", + "properties": { + "_meta": { + "additionalProperties": {}, + "description": "See [General fields: `_meta`](/specification/2025-11-25/basic/index#meta) for notes on `_meta` usage.", + "type": "object" + }, + "nextCursor": { + "description": "An opaque token representing the pagination position after the last returned result.\nIf present, there may be more results available.", + "type": "string" + }, + "tasks": { + "items": { + "$ref": "#/$defs/Task" + }, + "type": "array" + } + }, + "required": [ + "tasks" + ], + "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" + ], + "type": "object" + }, + "ListToolsResult": { + "description": "The server's response to a tools/list request from the client.", + "properties": { + "_meta": { + "additionalProperties": {}, + "description": "See [General fields: `_meta`](/specification/2025-11-25/basic/index#meta) for notes on `_meta` usage.", + "type": "object" + }, + "nextCursor": { + "description": "An opaque token representing the pagination position after the last returned result.\nIf present, there may be more results available.", + "type": "string" + }, + "tools": { + "items": { + "$ref": "#/$defs/Tool" + }, + "type": "array" + } + }, + "required": [ + "tools" + ], + "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. If no logging/setLevel request has been sent from the client, the server MAY decide which messages to send automatically.", + "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": { + "additionalProperties": {}, + "description": "See [General fields: `_meta`](/specification/2025-11-25/basic/index#meta) for notes on `_meta` usage.", + "type": "object" + }, + "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" + }, + "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" + }, + "NotificationParams": { + "properties": { + "_meta": { + "additionalProperties": {}, + "description": "See [General fields: `_meta`](/specification/2025-11-25/basic/index#meta) for notes on `_meta` usage.", + "type": "object" + } + }, + "type": "object" + }, + "NumberSchema": { + "properties": { + "default": { + "type": "integer" + }, + "description": { + "type": "string" + }, + "maximum": { + "type": "integer" + }, + "minimum": { + "type": "integer" + }, + "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" + ], + "type": "object" + }, + "PaginatedRequestParams": { + "description": "Common parameters for paginated requests.", + "properties": { + "_meta": { + "additionalProperties": {}, + "description": "See [General fields: `_meta`](/specification/2025-11-25/basic/index#meta) for notes on `_meta` usage.", + "properties": { + "progressToken": { + "$ref": "#/$defs/ProgressToken", + "description": "If specified, the caller is requesting out-of-band progress notifications for this request (as represented by 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." + } + }, + "type": "object" + }, + "cursor": { + "description": "An opaque token representing the current pagination position.\nIf provided, the server should return results starting after this cursor.", + "type": "string" + } + }, + "type": "object" + }, + "PaginatedResult": { + "properties": { + "_meta": { + "additionalProperties": {}, + "description": "See [General fields: `_meta`](/specification/2025-11-25/basic/index#meta) for notes on `_meta` usage.", + "type": "object" + }, + "nextCursor": { + "description": "An opaque token representing the pagination position after the last returned result.\nIf present, there may be more results available.", + "type": "string" + } + }, + "type": "object" + }, + "PingRequest": { + "description": "A ping, issued by either the server or the client, to check that the other party is still alive. The receiver must promptly respond, or else may be disconnected.", + "properties": { + "id": { + "$ref": "#/$defs/RequestId" + }, + "jsonrpc": { + "const": "2.0", + "type": "string" + }, + "method": { + "const": "ping", + "type": "string" + }, + "params": { + "$ref": "#/$defs/RequestParams" + } + }, + "required": [ + "id", + "jsonrpc", + "method" + ], + "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 `notifications/progress` notification.", + "properties": { + "_meta": { + "additionalProperties": {}, + "description": "See [General fields: `_meta`](/specification/2025-11-25/basic/index#meta) for notes on `_meta` usage.", + "type": "object" + }, + "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": { + "additionalProperties": {}, + "description": "See [General fields: `_meta`](/specification/2025-11-25/basic/index#meta) for notes on `_meta` usage.", + "type": "object" + }, + "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 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 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 may be issued by servers without any previous subscription from the client.", + "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 `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 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": { + "additionalProperties": {}, + "description": "See [General fields: `_meta`](/specification/2025-11-25/basic/index#meta) for notes on `_meta` usage.", + "properties": { + "progressToken": { + "$ref": "#/$defs/ProgressToken", + "description": "If specified, the caller is requesting out-of-band progress notifications for this request (as represented by 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." + } + }, + "type": "object" + }, + "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": [ + "uri" + ], + "type": "object" + }, + "ReadResourceResult": { + "description": "The server's response to a resources/read request from the client.", + "properties": { + "_meta": { + "additionalProperties": {}, + "description": "See [General fields: `_meta`](/specification/2025-11-25/basic/index#meta) for notes on `_meta` usage.", + "type": "object" + }, + "contents": { + "items": { + "anyOf": [ + { + "$ref": "#/$defs/TextResourceContents" + }, + { + "$ref": "#/$defs/BlobResourceContents" + } + ] + }, + "type": "array" + } + }, + "required": [ + "contents" + ], + "type": "object" + }, + "RelatedTaskMetadata": { + "description": "Metadata for associating messages with a task.\nInclude this in the `_meta` field under the key `io.modelcontextprotocol/related-task`.", + "properties": { + "taskId": { + "description": "The task identifier this message is associated with.", + "type": "string" + } + }, + "required": [ + "taskId" + ], + "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" + ] + }, + "RequestParams": { + "description": "Common params for any request.", + "properties": { + "_meta": { + "additionalProperties": {}, + "description": "See [General fields: `_meta`](/specification/2025-11-25/basic/index#meta) for notes on `_meta` usage.", + "properties": { + "progressToken": { + "$ref": "#/$defs/ProgressToken", + "description": "If specified, the caller is requesting out-of-band progress notifications for this request (as represented by 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." + } + }, + "type": "object" + } + }, + "type": "object" + }, + "Resource": { + "description": "A known resource that the server is capable of reading.", + "properties": { + "_meta": { + "additionalProperties": {}, + "description": "See [General fields: `_meta`](/specification/2025-11-25/basic/index#meta) for notes on `_meta` usage.", + "type": "object" + }, + "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 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": { + "additionalProperties": {}, + "description": "See [General fields: `_meta`](/specification/2025-11-25/basic/index#meta) for notes on `_meta` usage.", + "type": "object" + }, + "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 `resources/list` requests.", + "properties": { + "_meta": { + "additionalProperties": {}, + "description": "See [General fields: `_meta`](/specification/2025-11-25/basic/index#meta) for notes on `_meta` usage.", + "type": "object" + }, + "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 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 may be issued by servers without any previous subscription from the client.", + "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 parameters when working with resources.", + "properties": { + "_meta": { + "additionalProperties": {}, + "description": "See [General fields: `_meta`](/specification/2025-11-25/basic/index#meta) for notes on `_meta` usage.", + "properties": { + "progressToken": { + "$ref": "#/$defs/ProgressToken", + "description": "If specified, the caller is requesting out-of-band progress notifications for this request (as represented by 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." + } + }, + "type": "object" + }, + "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": [ + "uri" + ], + "type": "object" + }, + "ResourceTemplate": { + "description": "A template description for resources available on the server.", + "properties": { + "_meta": { + "additionalProperties": {}, + "description": "See [General fields: `_meta`](/specification/2025-11-25/basic/index#meta) for notes on `_meta` usage.", + "type": "object" + }, + "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 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 should only be sent if the client previously sent a resources/subscribe 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": { + "additionalProperties": {}, + "description": "See [General fields: `_meta`](/specification/2025-11-25/basic/index#meta) for notes on `_meta` usage.", + "type": "object" + }, + "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": {}, + "properties": { + "_meta": { + "additionalProperties": {}, + "description": "See [General fields: `_meta`](/specification/2025-11-25/basic/index#meta) for notes on `_meta` usage.", + "type": "object" + } + }, + "type": "object" + }, + "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": { + "additionalProperties": {}, + "description": "See [General fields: `_meta`](/specification/2025-11-25/basic/index#meta) for notes on `_meta` usage.", + "type": "object" + }, + "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" + }, + "RootsListChangedNotification": { + "description": "A notification from the client to the server, informing it that the list of roots has changed.\nThis notification should be sent whenever the client adds, removes, or modifies any root.\nThe server should then request an updated list of roots using the ListRootsRequest.", + "properties": { + "jsonrpc": { + "const": "2.0", + "type": "string" + }, + "method": { + "const": "notifications/roots/list_changed", + "type": "string" + }, + "params": { + "$ref": "#/$defs/NotificationParams" + } + }, + "required": [ + "jsonrpc", + "method" + ], + "type": "object" + }, + "SamplingMessage": { + "description": "Describes a message issued to or received from an LLM API.", + "properties": { + "_meta": { + "additionalProperties": {}, + "description": "See [General fields: `_meta`](/specification/2025-11-25/basic/index#meta) for notes on `_meta` usage.", + "type": "object" + }, + "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": { + "additionalProperties": true, + "description": "Present if the server supports argument autocompletion suggestions.", + "properties": {}, + "type": "object" + }, + "experimental": { + "additionalProperties": { + "additionalProperties": true, + "properties": {}, + "type": "object" + }, + "description": "Experimental, non-standard capabilities that the server supports.", + "type": "object" + }, + "logging": { + "additionalProperties": true, + "description": "Present if the server supports sending log messages to the client.", + "properties": {}, + "type": "object" + }, + "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" + }, + "tasks": { + "description": "Present if the server supports task-augmented requests.", + "properties": { + "cancel": { + "additionalProperties": true, + "description": "Whether this server supports tasks/cancel.", + "properties": {}, + "type": "object" + }, + "list": { + "additionalProperties": true, + "description": "Whether this server supports tasks/list.", + "properties": {}, + "type": "object" + }, + "requests": { + "description": "Specifies which request types can be augmented with tasks.", + "properties": { + "tools": { + "description": "Task support for tool-related requests.", + "properties": { + "call": { + "additionalProperties": true, + "description": "Whether the server supports task-augmented tools/call requests.", + "properties": {}, + "type": "object" + } + }, + "type": "object" + } + }, + "type": "object" + } + }, + "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/ResourceUpdatedNotification" + }, + { + "$ref": "#/$defs/PromptListChangedNotification" + }, + { + "$ref": "#/$defs/ToolListChangedNotification" + }, + { + "$ref": "#/$defs/TaskStatusNotification" + }, + { + "$ref": "#/$defs/LoggingMessageNotification" + }, + { + "$ref": "#/$defs/ElicitationCompleteNotification" + } + ] + }, + "ServerRequest": { + "anyOf": [ + { + "$ref": "#/$defs/PingRequest" + }, + { + "$ref": "#/$defs/GetTaskRequest" + }, + { + "$ref": "#/$defs/GetTaskPayloadRequest" + }, + { + "$ref": "#/$defs/CancelTaskRequest" + }, + { + "$ref": "#/$defs/ListTasksRequest" + }, + { + "$ref": "#/$defs/CreateMessageRequest" + }, + { + "$ref": "#/$defs/ListRootsRequest" + }, + { + "$ref": "#/$defs/ElicitRequest" + } + ] + }, + "ServerResult": { + "anyOf": [ + { + "$ref": "#/$defs/Result" + }, + { + "$ref": "#/$defs/InitializeResult" + }, + { + "$ref": "#/$defs/ListResourcesResult" + }, + { + "$ref": "#/$defs/ListResourceTemplatesResult" + }, + { + "$ref": "#/$defs/ReadResourceResult" + }, + { + "$ref": "#/$defs/ListPromptsResult" + }, + { + "$ref": "#/$defs/GetPromptResult" + }, + { + "$ref": "#/$defs/ListToolsResult" + }, + { + "$ref": "#/$defs/CallToolResult" + }, + { + "$ref": "#/$defs/GetTaskResult", + "description": "The response to a tasks/get request." + }, + { + "$ref": "#/$defs/GetTaskPayloadResult" + }, + { + "$ref": "#/$defs/CancelTaskResult", + "description": "The response to a tasks/cancel request." + }, + { + "$ref": "#/$defs/ListTasksResult" + }, + { + "$ref": "#/$defs/CompleteResult" + } + ] + }, + "SetLevelRequest": { + "description": "A request from the client to the server, to enable or adjust logging.", + "properties": { + "id": { + "$ref": "#/$defs/RequestId" + }, + "jsonrpc": { + "const": "2.0", + "type": "string" + }, + "method": { + "const": "logging/setLevel", + "type": "string" + }, + "params": { + "$ref": "#/$defs/SetLevelRequestParams" + } + }, + "required": [ + "id", + "jsonrpc", + "method", + "params" + ], + "type": "object" + }, + "SetLevelRequestParams": { + "description": "Parameters for a `logging/setLevel` request.", + "properties": { + "_meta": { + "additionalProperties": {}, + "description": "See [General fields: `_meta`](/specification/2025-11-25/basic/index#meta) for notes on `_meta` usage.", + "properties": { + "progressToken": { + "$ref": "#/$defs/ProgressToken", + "description": "If specified, the caller is requesting out-of-band progress notifications for this request (as represented by 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." + } + }, + "type": "object" + }, + "level": { + "$ref": "#/$defs/LoggingLevel", + "description": "The level of logging that the client wants to receive from the server. The server should send all logs at this level and higher (i.e., more severe) to the client as notifications/message." + } + }, + "required": [ + "level" + ], + "type": "object" + }, + "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" + }, + "SubscribeRequest": { + "description": "Sent from the client to request resources/updated notifications from the server whenever a particular resource changes.", + "properties": { + "id": { + "$ref": "#/$defs/RequestId" + }, + "jsonrpc": { + "const": "2.0", + "type": "string" + }, + "method": { + "const": "resources/subscribe", + "type": "string" + }, + "params": { + "$ref": "#/$defs/SubscribeRequestParams" + } + }, + "required": [ + "id", + "jsonrpc", + "method", + "params" + ], + "type": "object" + }, + "SubscribeRequestParams": { + "description": "Parameters for a `resources/subscribe` request.", + "properties": { + "_meta": { + "additionalProperties": {}, + "description": "See [General fields: `_meta`](/specification/2025-11-25/basic/index#meta) for notes on `_meta` usage.", + "properties": { + "progressToken": { + "$ref": "#/$defs/ProgressToken", + "description": "If specified, the caller is requesting out-of-band progress notifications for this request (as represented by 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." + } + }, + "type": "object" + }, + "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": [ + "uri" + ], + "type": "object" + }, + "Task": { + "description": "Data associated with a task.", + "properties": { + "createdAt": { + "description": "ISO 8601 timestamp when the task was created.", + "type": "string" + }, + "lastUpdatedAt": { + "description": "ISO 8601 timestamp when the task was last updated.", + "type": "string" + }, + "pollInterval": { + "description": "Suggested polling interval in milliseconds.", + "type": "integer" + }, + "status": { + "$ref": "#/$defs/TaskStatus", + "description": "Current task state." + }, + "statusMessage": { + "description": "Optional human-readable message describing the current task state.\nThis can provide context for any status, including:\n- Reasons for \"cancelled\" status\n- Summaries for \"completed\" status\n- Diagnostic information for \"failed\" status (e.g., error details, what went wrong)", + "type": "string" + }, + "taskId": { + "description": "The task identifier.", + "type": "string" + }, + "ttl": { + "description": "Actual retention duration from creation in milliseconds, null for unlimited.", + "type": [ + "integer", + "null" + ] + } + }, + "required": [ + "createdAt", + "lastUpdatedAt", + "status", + "taskId", + "ttl" + ], + "type": "object" + }, + "TaskAugmentedRequestParams": { + "description": "Common params for any task-augmented request.", + "properties": { + "_meta": { + "additionalProperties": {}, + "description": "See [General fields: `_meta`](/specification/2025-11-25/basic/index#meta) for notes on `_meta` usage.", + "properties": { + "progressToken": { + "$ref": "#/$defs/ProgressToken", + "description": "If specified, the caller is requesting out-of-band progress notifications for this request (as represented by 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." + } + }, + "type": "object" + }, + "task": { + "$ref": "#/$defs/TaskMetadata", + "description": "If specified, the caller is requesting task-augmented execution for this request.\nThe request will return a CreateTaskResult immediately, and the actual result can be\nretrieved later via tasks/result.\n\nTask augmentation is subject to capability negotiation - receivers MUST declare support\nfor task augmentation of specific request types in their capabilities." + } + }, + "type": "object" + }, + "TaskMetadata": { + "description": "Metadata for augmenting a request with task execution.\nInclude this in the `task` field of the request parameters.", + "properties": { + "ttl": { + "description": "Requested duration in milliseconds to retain task from creation.", + "type": "integer" + } + }, + "type": "object" + }, + "TaskStatus": { + "description": "The status of a task.", + "enum": [ + "cancelled", + "completed", + "failed", + "input_required", + "working" + ], + "type": "string" + }, + "TaskStatusNotification": { + "description": "An optional notification from the receiver to the requestor, informing them that a task's status has changed. Receivers are not required to send these notifications.", + "properties": { + "jsonrpc": { + "const": "2.0", + "type": "string" + }, + "method": { + "const": "notifications/tasks/status", + "type": "string" + }, + "params": { + "$ref": "#/$defs/TaskStatusNotificationParams" + } + }, + "required": [ + "jsonrpc", + "method", + "params" + ], + "type": "object" + }, + "TaskStatusNotificationParams": { + "allOf": [ + { + "$ref": "#/$defs/NotificationParams" + }, + { + "$ref": "#/$defs/Task" + } + ], + "description": "Parameters for a `notifications/tasks/status` notification." + }, + "TextContent": { + "description": "Text provided to or from an LLM.", + "properties": { + "_meta": { + "additionalProperties": {}, + "description": "See [General fields: `_meta`](/specification/2025-11-25/basic/index#meta) for notes on `_meta` usage.", + "type": "object" + }, + "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": { + "additionalProperties": {}, + "description": "See [General fields: `_meta`](/specification/2025-11-25/basic/index#meta) for notes on `_meta` usage.", + "type": "object" + }, + "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": { + "additionalProperties": {}, + "description": "See [General fields: `_meta`](/specification/2025-11-25/basic/index#meta) for notes on `_meta` usage.", + "type": "object" + }, + "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" + }, + "execution": { + "$ref": "#/$defs/ToolExecution", + "description": "Execution-related properties for this tool." + }, + "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": { + "description": "A JSON Schema object defining the expected parameters for the tool.", + "properties": { + "$schema": { + "type": "string" + }, + "properties": { + "additionalProperties": { + "additionalProperties": true, + "properties": {}, + "type": "object" + }, + "type": "object" + }, + "required": { + "items": { + "type": "string" + }, + "type": "array" + }, + "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": { + "description": "An optional JSON Schema object defining the structure of the tool's output returned in\nthe structuredContent field of a CallToolResult.\n\nDefaults to JSON Schema 2020-12 when no explicit $schema is provided.\nCurrently restricted to type: \"object\" at the root level.", + "properties": { + "$schema": { + "type": "string" + }, + "properties": { + "additionalProperties": { + "additionalProperties": true, + "properties": {}, + "type": "object" + }, + "type": "object" + }, + "required": { + "items": { + "type": "string" + }, + "type": "array" + }, + "type": { + "const": "object", + "type": "string" + } + }, + "required": [ + "type" + ], + "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 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 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" + }, + "ToolExecution": { + "description": "Execution-related properties for a tool.", + "properties": { + "taskSupport": { + "description": "Indicates whether this tool supports task-augmented execution.\nThis allows clients to handle long-running operations through polling\nthe task system.\n\n- \"forbidden\": Tool does not support task-augmented execution (default when absent)\n- \"optional\": Tool may support task-augmented execution\n- \"required\": Tool requires task-augmented execution\n\nDefault: \"forbidden\"", + "enum": [ + "forbidden", + "optional", + "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 may be issued by servers without any previous subscription from the client.", + "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": { + "additionalProperties": {}, + "description": "Optional metadata about the tool result. Clients SHOULD preserve this field when\nincluding tool results in subsequent sampling requests to enable caching optimizations.\n\nSee [General fields: `_meta`](/specification/2025-11-25/basic/index#meta) for notes on `_meta` usage.", + "type": "object" + }, + "content": { + "description": "The unstructured result content of the tool use.\n\nThis has the same format as 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": { + "additionalProperties": {}, + "description": "An optional structured result object.\n\nIf the tool defined an outputSchema, this SHOULD conform to that schema.", + "type": "object" + }, + "toolUseId": { + "description": "The ID of the tool use this result corresponds to.\n\nThis MUST match the ID from a previous 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": { + "additionalProperties": {}, + "description": "Optional metadata about the tool use. Clients SHOULD preserve this field when\nincluding tool uses in subsequent sampling requests to enable caching optimizations.\n\nSee [General fields: `_meta`](/specification/2025-11-25/basic/index#meta) for notes on `_meta` usage.", + "type": "object" + }, + "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" + }, + "URLElicitationRequiredError": { + "description": "An error response that indicates that the server requires the client to provide additional information via an elicitation request.", + "properties": { + "error": { + "allOf": [ + { + "$ref": "#/$defs/Error" + }, + { + "properties": { + "code": { + "const": -32042, + "type": "integer" + }, + "data": { + "additionalProperties": {}, + "properties": { + "elicitations": { + "items": { + "$ref": "#/$defs/ElicitRequestURLParams" + }, + "type": "array" + } + }, + "required": [ + "elicitations" + ], + "type": "object" + } + }, + "required": [ + "code", + "data" + ], + "type": "object" + } + ] + }, + "id": { + "$ref": "#/$defs/RequestId" + }, + "jsonrpc": { + "const": "2.0", + "type": "string" + } + }, + "required": [ + "error", + "jsonrpc" + ], + "type": "object" + }, + "UnsubscribeRequest": { + "description": "Sent from the client to request cancellation of resources/updated notifications from the server. This should follow a previous resources/subscribe request.", + "properties": { + "id": { + "$ref": "#/$defs/RequestId" + }, + "jsonrpc": { + "const": "2.0", + "type": "string" + }, + "method": { + "const": "resources/unsubscribe", + "type": "string" + }, + "params": { + "$ref": "#/$defs/UnsubscribeRequestParams" + } + }, + "required": [ + "id", + "jsonrpc", + "method", + "params" + ], + "type": "object" + }, + "UnsubscribeRequestParams": { + "description": "Parameters for a `resources/unsubscribe` request.", + "properties": { + "_meta": { + "additionalProperties": {}, + "description": "See [General fields: `_meta`](/specification/2025-11-25/basic/index#meta) for notes on `_meta` usage.", + "properties": { + "progressToken": { + "$ref": "#/$defs/ProgressToken", + "description": "If specified, the caller is requesting out-of-band progress notifications for this request (as represented by 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." + } + }, + "type": "object" + }, + "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": [ + "uri" + ], + "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/packages/core/test/corpus/schema-twins/2026-07-28.schema.json b/packages/core/test/corpus/schema-twins/2026-07-28.schema.json new file mode 100644 index 0000000000..5ce9df12e4 --- /dev/null +++ b/packages/core/test/corpus/schema-twins/2026-07-28.schema.json @@ -0,0 +1,3881 @@ +{ + "$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/MetaObject" + }, + "cacheScope": { + "description": "Indicates the intended scope of the cached response, analogous to HTTP\n`Cache-Control: public` vs `Cache-Control: private`.\n\n- `\"public\"`: Any client or intermediary (e.g., shared gateway, proxy)\n MAY cache the response and serve it to any user.\n- `\"private\"`: Only the requesting user's client MAY cache the response.\n Shared caches (e.g., multi-tenant gateways) MUST NOT serve a cached\n copy to a different user.", + "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/MetaObject" + }, + "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 can be sent by either side to indicate that it is cancelling a previously-issued 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/MetaObject" + }, + "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 previously issued in the same direction." + } + }, + "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": { + "anyOf": [ + { + "$ref": "#/$defs/CancelledNotification" + }, + { + "$ref": "#/$defs/ProgressNotification" + } + ] + }, + "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/MetaObject" + }, + "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/MetaObject" + }, + "cacheScope": { + "description": "Indicates the intended scope of the cached response, analogous to HTTP\n`Cache-Control: public` vs `Cache-Control: private`.\n\n- `\"public\"`: Any client or intermediary (e.g., shared gateway, proxy)\n MAY cache the response and serve it to any user.\n- `\"private\"`: Only the requesting user's client MAY cache the response.\n Shared caches (e.g., multi-tenant gateways) MUST NOT serve a cached\n copy to a different user.", + "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" + }, + "serverInfo": { + "$ref": "#/$defs/Implementation", + "description": "Information about the server software implementation." + }, + "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", + "serverInfo", + "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": { + "elicitationId": { + "description": "The ID of the elicitation, which must be unique within the context of the server.\nThe client MUST treat this ID as an opaque value.", + "type": "string" + }, + "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": [ + "elicitationId", + "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" + }, + "ElicitationCompleteNotification": { + "description": "An optional notification from the server to the client, informing it of a completion of a out-of-band elicitation request.", + "properties": { + "jsonrpc": { + "const": "2.0", + "type": "string" + }, + "method": { + "const": "notifications/elicitation/complete", + "type": "string" + }, + "params": { + "$ref": "#/$defs/ElicitationCompleteNotificationParams" + } + }, + "required": [ + "jsonrpc", + "method", + "params" + ], + "type": "object" + }, + "ElicitationCompleteNotificationParams": { + "description": "Parameters for a {@link ElicitationCompleteNotificationnotifications/elicitation/complete} notification.", + "properties": { + "_meta": { + "$ref": "#/$defs/MetaObject" + }, + "elicitationId": { + "description": "The ID of the elicitation that completed.", + "type": "string" + } + }, + "required": [ + "elicitationId" + ], + "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/MetaObject" + }, + "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" + }, + "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/MetaObject" + }, + "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/MetaObject" + }, + "cacheScope": { + "description": "Indicates the intended scope of the cached response, analogous to HTTP\n`Cache-Control: public` vs `Cache-Control: private`.\n\n- `\"public\"`: Any client or intermediary (e.g., shared gateway, proxy)\n MAY cache the response and serve it to any user.\n- `\"private\"`: Only the requesting user's client MAY cache the response.\n Shared caches (e.g., multi-tenant gateways) MUST NOT serve a cached\n copy to a different user.", + "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/MetaObject" + }, + "cacheScope": { + "description": "Indicates the intended scope of the cached response, analogous to HTTP\n`Cache-Control: public` vs `Cache-Control: private`.\n\n- `\"public\"`: Any client or intermediary (e.g., shared gateway, proxy)\n MAY cache the response and serve it to any user.\n- `\"private\"`: Only the requesting user's client MAY cache the response.\n Shared caches (e.g., multi-tenant gateways) MUST NOT serve a cached\n copy to a different user.", + "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/MetaObject" + }, + "cacheScope": { + "description": "Indicates the intended scope of the cached response, analogous to HTTP\n`Cache-Control: public` vs `Cache-Control: private`.\n\n- `\"public\"`: Any client or intermediary (e.g., shared gateway, proxy)\n MAY cache the response and serve it to any user.\n- `\"private\"`: Only the requesting user's client MAY cache the response.\n Shared caches (e.g., multi-tenant gateways) MUST NOT serve a cached\n copy to a different user.", + "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": { + "$ref": "#/$defs/RequestParams" + } + }, + "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/MetaObject" + }, + "cacheScope": { + "description": "Indicates the intended scope of the cached response, analogous to HTTP\n`Cache-Control: public` vs `Cache-Control: private`.\n\n- `\"public\"`: Any client or intermediary (e.g., shared gateway, proxy)\n MAY cache the response and serve it to any user.\n- `\"private\"`: Only the requesting user's client MAY cache the response.\n Shared caches (e.g., multi-tenant gateways) MUST NOT serve a cached\n copy to a different user.", + "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/MetaObject" + }, + "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} (`-32003`).", + "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": -32003, + "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" + }, + "NotificationParams": { + "description": "Common params for any notification.", + "properties": { + "_meta": { + "$ref": "#/$defs/MetaObject" + } + }, + "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/MetaObject" + }, + "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/MetaObject" + }, + "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 may be issued by servers without any previous subscription from the client.", + "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/MetaObject" + }, + "cacheScope": { + "description": "Indicates the intended scope of the cached response, analogous to HTTP\n`Cache-Control: public` vs `Cache-Control: private`.\n\n- `\"public\"`: Any client or intermediary (e.g., shared gateway, proxy)\n MAY cache the response and serve it to any user.\n- `\"private\"`: Only the requesting user's client MAY cache the response.\n Shared caches (e.g., multi-tenant gateways) MUST NOT serve a cached\n copy to a different user.", + "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. Required.\n\nThe {@link Implementation} schema requires `name` and `version`; other\nfields are optional." + }, + "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/clientInfo", + "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 may be issued by servers without any previous subscription from the client.", + "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/MetaObject" + }, + "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/MetaObject" + }, + "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" + }, + "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" + }, + { + "$ref": "#/$defs/ElicitationCompleteNotification" + } + ] + }, + "ServerResult": { + "anyOf": [ + { + "$ref": "#/$defs/Result" + }, + { + "$ref": "#/$defs/InputRequiredResult" + }, + { + "$ref": "#/$defs/DiscoverResult" + }, + { + "$ref": "#/$defs/ListResourcesResult" + }, + { + "$ref": "#/$defs/ListResourceTemplatesResult" + }, + { + "$ref": "#/$defs/ReadResourceResult" + }, + { + "$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 as the first message on a\n{@link SubscriptionsListenRequestsubscriptions/listen} stream to acknowledge\nthat the subscription has been established and to report which notification\ntypes it agreed to honor.", + "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/MetaObject" + }, + "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" + }, + "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\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 may be issued by servers without any previous subscription from the client.", + "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": -32004, + "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/packages/core/test/corpus/schema-twins/manifest.json b/packages/core/test/corpus/schema-twins/manifest.json new file mode 100644 index 0000000000..1b21d36b3d --- /dev/null +++ b/packages/core/test/corpus/schema-twins/manifest.json @@ -0,0 +1,19 @@ +{ + "comment": "Vendored schema.json twins (TEST-ONLY conformance oracles; never bundled, never runtime). RAW upstream bytes - never reformat: each file is locked to the sha256/bytes below by schemaTwinConformance. Refresh via `pnpm fetch:schema-twins [sha]`, ATOMICALLY with the matching spec.types anchor (see packages/core/src/types/README.md lifecycle rule 4).", + "source": { + "repository": "modelcontextprotocol/modelcontextprotocol", + "commit": "0168c57fc74aba6e6dcf8f0b7191db3caaa5ad65" + }, + "files": { + "2026-07-28": { + "sha256": "afaf886c06dd8d3cbdd556d81b6483b9018112aaf7ee284fa116eca58baf54fc", + "bytes": 172822, + "upstreamPath": "schema/draft/schema.json" + }, + "2025-11-25": { + "sha256": "7b2d96fd95efd2216aa953606b83f5a740ddeaa5ebd3a5d27b45a8296545a118", + "bytes": 174326, + "upstreamPath": "schema/2025-11-25/schema.json" + } + } +} diff --git a/packages/core/test/corpus/specCorpus.test.ts b/packages/core/test/corpus/specCorpus.test.ts new file mode 100644 index 0000000000..f36586f9e7 --- /dev/null +++ b/packages/core/test/corpus/specCorpus.test.ts @@ -0,0 +1,202 @@ +/** + * Spec example corpus — accept-side fixtures parsed through the SDK's wire schemas. + * + * Two corpora, one harness: + * + * - `fixtures/2026-07-28/` is VENDORED from the spec repository's draft + * example set (`schema/draft/examples/`), regenerated only via + * `pnpm fetch:spec-examples` (provenance in its manifest.json). Every + * example directory is named after a spec type; each file is a canonical + * instance of that type. + * - `fixtures/2025-11-25/` is HAND-BUILT and FROZEN: upstream ships no + * example corpus for the released 2025-11-25 revision, so these fixtures + * pin representative 2025-era wire shapes (including the task wire surface + * that revision defines). Do not edit them casually — they are the + * accept-side net for any future change to how 2025-era traffic parses. + * + * Directory-name → schema mapping is mechanical (`Schema`), with two + * structural exceptions (JSON-RPC response envelopes and bare error objects) + * and an explicit pending list for draft vocabulary the SDK does not model + * yet. The pending list is stale-checked in both directions: a pending entry + * whose schema appears must be removed, and an unmapped directory that is not + * pending fails loudly — no silent skips. + * + * Rejection-side fixtures are deliberately NOT here: accept-only corpora are + * blind to accept→reject deltas, so rejections are routed through real + * dispatch in specCorpusDispatch.test.ts. + */ +import { readdirSync, readFileSync, statSync } from 'node:fs'; +import { join } from 'node:path'; + +import { describe, expect, test } from 'vitest'; +import * as z from 'zod/v4'; + +import { + CreateMessageResultSchema, + CreateMessageResultWithToolsSchema, + JSONRPCErrorResponseSchema, + JSONRPCResultResponseSchema +} from '../../src/types/schemas.js'; +import * as schemas from '../../src/types/schemas.js'; +// Era routing (Q1 increment 2): each corpus revision resolves through its own +// wire-era module first — 2025 fixtures may use 2025-only vocabulary (tasks), +// 2026 fixtures use 2026-only vocabulary (envelope, discover) — then falls +// back to the shared neutral payload schemas. +import * as wire2025 from '../../src/wire/rev2025-11-25/schemas.js'; +import * as wire2026 from '../../src/wire/rev2026-07-28/schemas.js'; + +const FIXTURES_ROOT = join(__dirname, 'fixtures'); + +/** JSON-RPC error-object example directories (bare `{code, message, data?}` shapes). */ +const ERROR_OBJECT_DIRS = new Set([ + 'InternalError', + 'InvalidParamsError', + 'MethodNotFoundError', + 'MissingRequiredClientCapabilityError', + 'ParseError', + 'UnsupportedProtocolVersionError' +]); + +/** + * Draft (2026-07-28) vocabulary the SDK does not model yet, at directory + * granularity. Each entry names the reason; the harness asserts the schema is + * genuinely absent so a stale entry (vocabulary landed but still listed) + * fails loudly. These burn down as the corresponding features land. + */ +const PENDING_2026: Record = { + InputRequests: 'multi-round-trip request vocabulary (SEP-2322) is not modeled yet', + InputRequiredResult: 'multi-round-trip request vocabulary (SEP-2322) is not modeled yet', + InputResponses: 'multi-round-trip request vocabulary (SEP-2322) is not modeled yet', + SubscriptionsAcknowledgedNotification: 'subscriptions/listen vocabulary (SEP-1865) is not modeled yet', + SubscriptionsListenRequest: 'subscriptions/listen vocabulary (SEP-1865) is not modeled yet' +}; + +/** + * Individual draft examples whose vocabulary the SDK does not accept yet + * (file granularity — the directory's schema exists but this instance uses a + * draft-only widening). Stale-checked: each listed file must actually FAIL to + * parse, so the entry is removed the moment the widening lands. + */ +const PENDING_2026_FILES: Record = { + // (empty — the SEP-2549 array-shape widenings burned when the 2026-era + // wire module landed anchor-exact Tool/CallToolResult forks; the two + // examples are real pins now.) +}; + +type AnyZod = z.ZodType; + +const ERA_SCHEMAS: Record> = { + '2025-11-25': wire2025 as Record, + '2026-07-28': wire2026 as Record +}; + +function schemaFor(revision: string, dir: string, fixture: unknown): AnyZod | undefined { + if (ERROR_OBJECT_DIRS.has(dir)) { + // The upstream error examples mix bare `{code, message, data?}` objects + // with full JSON-RPC error responses — pick by shape. + const isEnveloped = typeof fixture === 'object' && fixture !== null && 'jsonrpc' in fixture; + return isEnveloped ? (JSONRPCErrorResponseSchema as AnyZod) : (JSONRPCErrorResponseSchema.shape.error as AnyZod); + } + if (dir.endsWith('ResultResponse')) return JSONRPCResultResponseSchema as AnyZod; + if (dir === 'CreateMessageResult') { + // The SDK models this spec type as two schemas (single-content and + // tool-use array content); an example instance may be either. + return z.union([CreateMessageResultSchema, CreateMessageResultWithToolsSchema]) as AnyZod; + } + const eraSchema = ERA_SCHEMAS[revision]?.[`${dir}Schema`]; + if (eraSchema !== undefined) return eraSchema as AnyZod; + return (schemas as Record)[`${dir}Schema`] as AnyZod | undefined; +} + +function listTypeDirs(revision: string): string[] { + const root = join(FIXTURES_ROOT, revision); + return readdirSync(root) + .filter(entry => statSync(join(root, entry)).isDirectory()) + .sort(); +} + +function listFixtures(revision: string, dir: string): string[] { + return readdirSync(join(FIXTURES_ROOT, revision, dir)) + .filter(file => file.endsWith('.json')) + .sort(); +} + +function loadFixture(revision: string, dir: string, file: string): unknown { + return JSON.parse(readFileSync(join(FIXTURES_ROOT, revision, dir, file), 'utf8')); +} + +describe.each(['2025-11-25', '2026-07-28'] as const)('spec example corpus %s', revision => { + const typeDirs = listTypeDirs(revision); + const pending = revision === '2026-07-28' ? PENDING_2026 : {}; + + const pendingFiles = revision === '2026-07-28' ? PENDING_2026_FILES : {}; + + test('every example directory is mapped to a schema or explicitly pending', () => { + const unmapped = typeDirs.filter(dir => !(dir in pending) && schemaFor(revision, dir, {}) === undefined); + expect(unmapped, 'unmapped example directories — map them or add a documented pending entry').toEqual([]); + }); + + test('pending entries are not stale (their vocabulary is still unmodeled)', () => { + const stale = Object.keys(pending).filter(dir => schemaFor(revision, dir, {}) !== undefined); + expect(stale, 'pending entries whose schema now exists — wire the fixtures and remove the entry').toEqual([]); + // Pending entries must refer to directories that actually exist. + const missing = Object.keys(pending).filter(dir => !typeDirs.includes(dir)); + expect(missing, 'pending entries without a fixture directory').toEqual([]); + + const missingFiles = Object.keys(pendingFiles).filter(relPath => { + const [dir, file] = relPath.split('/'); + if (dir === undefined || file === undefined) return true; + return !typeDirs.includes(dir) || !listFixtures(revision, dir).includes(file); + }); + expect(missingFiles, 'pending file entries without a fixture file').toEqual([]); + }); + + const mappedDirs = typeDirs.filter(dir => !(dir in pending)); + describe.each(mappedDirs)('%s', dir => { + test.each(listFixtures(revision, dir))('%s parses', file => { + const fixture = loadFixture(revision, dir, file); + const schema = schemaFor(revision, dir, fixture); + expect(schema).toBeDefined(); + const parsed = schema!.safeParse(fixture); + const pendingReason = pendingFiles[`${dir}/${file}`]; + if (pendingReason !== undefined) { + // Stale-check: a pending file that parses means the widening + // landed — remove the entry so the example becomes a real pin. + expect(parsed.success, `pending entry is stale ('${dir}/${file}' now parses): ${pendingReason}`).toBe(false); + return; + } + expect(parsed.success, parsed.success ? undefined : `'${dir}/${file}' failed to parse:\n${parsed.error}`).toBe(true); + }); + }); +}); + +describe('corpus inventory pins', () => { + test('the vendored 2026-07-28 corpus matches its manifest (provenance + drift pin)', () => { + const manifest = JSON.parse(readFileSync(join(FIXTURES_ROOT, '2026-07-28', 'manifest.json'), 'utf8')) as { + revision: string; + source: { commit: string }; + directoryCount: number; + fileCount: number; + directories: Record; + }; + expect(manifest.revision).toBe('2026-07-28'); + + const dirs = listTypeDirs('2026-07-28'); + expect(dirs).toEqual(Object.keys(manifest.directories).sort()); + const fileCount = dirs.reduce((sum, dir) => sum + listFixtures('2026-07-28', dir).length, 0); + expect(fileCount).toBe(manifest.fileCount); + + // The corpus size at the pinned spec commit. A change here means the + // vendored corpus was regenerated — review the delta deliberately. + expect(manifest.directoryCount).toBe(86); + expect(manifest.fileCount).toBe(127); + }); + + test('the frozen 2025-11-25 corpus keeps its inventory', () => { + const dirs = listTypeDirs('2025-11-25'); + const fileCount = dirs.reduce((sum, dir) => sum + listFixtures('2025-11-25', dir).length, 0); + // Hand-built and frozen: growing it is welcome (raise the pin in the + // same change); silent shrinkage is not. + expect(fileCount).toBe(47); + }); +}); diff --git a/packages/core/test/corpus/specCorpusDispatch.test.ts b/packages/core/test/corpus/specCorpusDispatch.test.ts new file mode 100644 index 0000000000..88859aa71a --- /dev/null +++ b/packages/core/test/corpus/specCorpusDispatch.test.ts @@ -0,0 +1,121 @@ +/** + * Rejection-side corpus, routed through real dispatch. + * + * Accept-only corpora (specCorpus.test.ts) are blind to accept→reject deltas: + * a schema split or strictness change that turns previously-accepted traffic + * into rejections (or vice versa) never fails a parse-success fixture. These + * fixtures therefore drive raw JSON-RPC messages through a connected + * Protocol — the transport boundary, classification, handler lookup, and + * per-method parse exactly as production dispatch runs them — and pin the + * observable outcome of each: + * + * - `error-response`: an error response with the pinned code is sent back + * - `onerror`: no response; the failure surfaces via onerror + * - `ignored`: no response and no onerror (silent drop) + * - `result-response`: a result response is sent (accept-side sanity) + * + * The fixtures record TODAY's dispatch behavior. When a deliberate change + * moves the accept/reject line, the affected fixture turns red and must be + * updated in the same change (with its changeset / migration entry). + */ +import { readdirSync, readFileSync } from 'node:fs'; +import { join } from 'node:path'; + +import { describe, expect, test } from 'vitest'; + +import { Protocol } from '../../src/shared/protocol.js'; +import type { BaseContext } from '../../src/shared/protocol.js'; +import { InMemoryTransport } from '../../src/util/inMemory.js'; +import type { JSONRPCMessage } from '../../src/types/index.js'; + +const REJECTION_DIR = join(__dirname, 'fixtures', 'rejection'); + +interface DispatchFixture { + description: string; + message: unknown; + expect: 'error-response' | 'onerror' | 'ignored' | 'result-response'; + errorCode?: number; +} + +class ReceiverProtocol extends Protocol { + protected assertCapabilityForMethod(): void {} + protected assertNotificationCapability(): void {} + protected assertRequestHandlerCapability(): void {} + protected buildContext(ctx: BaseContext): BaseContext { + return ctx; + } +} + +interface Outcome { + responses: JSONRPCMessage[]; + errors: Error[]; +} + +/** Connect a receiver, inject the raw message from the peer side, observe. */ +async function dispatch(message: unknown): Promise { + const [peerTx, receiverTx] = InMemoryTransport.createLinkedPair(); + + const receiver = new ReceiverProtocol(); + const errors: Error[] = []; + receiver.onerror = error => void errors.push(error); + // One registered spec handler so the accept-side fixture has a target. + receiver.setRequestHandler('tools/call', async request => ({ + content: [{ type: 'text', text: String(request.params?.name) }] + })); + await receiver.connect(receiverTx); + + const responses: JSONRPCMessage[] = []; + peerTx.onmessage = received => void responses.push(received); + await peerTx.start(); + + // The InMemoryTransport is typed for valid messages; the cast is the + // point — raw bytes can always carry these shapes to dispatch. + await peerTx.send(message as JSONRPCMessage); + + // Dispatch is asynchronous (handlers run in promise chains); settle. + await new Promise(resolve => setTimeout(resolve, 25)); + + await receiver.close(); + return { responses, errors }; +} + +const fixtureFiles = readdirSync(REJECTION_DIR) + .filter(file => file.endsWith('.json')) + .sort(); + +describe('dispatch-routed corpus (rejection side + accept sanity)', () => { + test('the corpus is present', () => { + expect(fixtureFiles.length).toBeGreaterThanOrEqual(13); + }); + + test.each(fixtureFiles)('%s', async file => { + const fixture = JSON.parse(readFileSync(join(REJECTION_DIR, file), 'utf8')) as DispatchFixture; + const outcome = await dispatch(fixture.message); + + switch (fixture.expect) { + case 'error-response': { + expect(outcome.responses, fixture.description).toHaveLength(1); + const response = outcome.responses[0] as { error?: { code: number } }; + expect(response.error, `expected an error response: ${fixture.description}`).toBeDefined(); + expect(response.error?.code, fixture.description).toBe(fixture.errorCode); + break; + } + case 'result-response': { + expect(outcome.responses, fixture.description).toHaveLength(1); + const response = outcome.responses[0] as { result?: unknown }; + expect(response.result, `expected a result response: ${fixture.description}`).toBeDefined(); + break; + } + case 'onerror': { + expect(outcome.responses, `expected no response: ${fixture.description}`).toHaveLength(0); + expect(outcome.errors.length, `expected an out-of-band error: ${fixture.description}`).toBeGreaterThan(0); + break; + } + case 'ignored': { + expect(outcome.responses, `expected no response: ${fixture.description}`).toHaveLength(0); + expect(outcome.errors, `expected no out-of-band error: ${fixture.description}`).toHaveLength(0); + break; + } + } + }); +}); diff --git a/packages/core/test/packageTopologyPins.test.ts b/packages/core/test/packageTopologyPins.test.ts new file mode 100644 index 0000000000..9a12a303b6 --- /dev/null +++ b/packages/core/test/packageTopologyPins.test.ts @@ -0,0 +1,146 @@ +/** + * Behavior-surface pins: workspace package topology and export maps. + * + * The published surface of the SDK is the set of public packages and their + * export-map entries. Consumers resolve deep subpaths through these maps, so + * adding, removing, or renaming an entry — or flipping a private flag — is a + * consumer-visible change. This pins the manifest-level topology: every change + * to it must be deliberate (update the pin, add a changeset, and document the + * migration). Runtime resolvability of the built entries is covered by the + * integration test workspace. + * + * See docs/behavior-surface-pins.md for the maintenance protocol. + */ +import { existsSync, readdirSync, readFileSync } from 'node:fs'; +import { dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +import { describe, expect, test } from 'vitest'; + +const packagesDir = join(dirname(fileURLToPath(import.meta.url)), '..', '..'); + +interface PackageManifest { + name: string; + private?: boolean; + type?: string; + files?: string[]; + bin?: Record; + exports?: Record; +} + +function readManifest(relativeDir: string): PackageManifest { + return JSON.parse(readFileSync(join(packagesDir, relativeDir, 'package.json'), 'utf8')) as PackageManifest; +} + +/** dir (relative to packages/) → expected manifest shape */ +const PUBLIC_PACKAGES: Record }> = { + client: { + name: '@modelcontextprotocol/client', + exportKeys: ['.', './stdio', './validators/ajv', './validators/cf-worker', './_shims'] + }, + server: { + name: '@modelcontextprotocol/server', + exportKeys: ['.', './stdio', './validators/ajv', './validators/cf-worker', './_shims'] + }, + 'server-legacy': { + name: '@modelcontextprotocol/server-legacy', + exportKeys: ['.', './sse', './auth'] + }, + 'middleware/express': { name: '@modelcontextprotocol/express', exportKeys: ['.'] }, + 'middleware/fastify': { name: '@modelcontextprotocol/fastify', exportKeys: ['.'] }, + 'middleware/hono': { name: '@modelcontextprotocol/hono', exportKeys: ['.'] }, + 'middleware/node': { name: '@modelcontextprotocol/node', exportKeys: ['.'] }, + codemod: { + name: '@modelcontextprotocol/codemod', + exportKeys: ['.'], + bin: { 'mcp-codemod': './dist/cli.mjs' } + } +}; + +describe('public package topology', () => { + for (const [dir, expected] of Object.entries(PUBLIC_PACKAGES)) { + describe(expected.name, () => { + const manifest = readManifest(dir); + + test('is published under the pinned name', () => { + expect(manifest.name).toBe(expected.name); + expect(manifest.private).not.toBe(true); + }); + + test('export-map keys are pinned exactly', () => { + expect(Object.keys(manifest.exports ?? {})).toEqual(expected.exportKeys); + }); + + test('ships ESM only', () => { + expect(manifest.type).toBe('module'); + // No entry may grow a 'require' condition: the v2 packages are + // ESM-only by design (a CJS build would be a new public surface). + const conditionsOf = (entry: unknown): string[] => + entry !== null && typeof entry === 'object' + ? Object.entries(entry).flatMap(([key, value]) => [key, ...conditionsOf(value)]) + : []; + for (const entry of Object.values(manifest.exports ?? {})) { + expect(conditionsOf(entry)).not.toContain('require'); + } + }); + + test('publishes only dist', () => { + expect(manifest.files).toEqual(['dist']); + }); + + if (expected.bin) { + test('bin entries are pinned', () => { + expect(manifest.bin).toEqual(expected.bin); + }); + } else { + test('declares no bin entries', () => { + expect(manifest.bin).toBeUndefined(); + }); + } + }); + } +}); + +describe('the package set itself is pinned', () => { + /** Every directory under packages/ (one level, plus middleware/*) holding a package.json. */ + function discoverManifestDirs(): string[] { + const dirs: string[] = []; + for (const entry of readdirSync(packagesDir, { withFileTypes: true })) { + if (!entry.isDirectory()) { + continue; + } + if (existsSync(join(packagesDir, entry.name, 'package.json'))) { + dirs.push(entry.name); + continue; + } + for (const nested of readdirSync(join(packagesDir, entry.name), { withFileTypes: true })) { + if (nested.isDirectory() && existsSync(join(packagesDir, entry.name, nested.name, 'package.json'))) { + dirs.push(`${entry.name}/${nested.name}`); + } + } + } + return dirs.sort(); + } + + test('every manifest under packages/ is either a pinned public package or core', () => { + // The workspace glob (packages/**/*) auto-adopts any new directory and + // the changesets config publishes every non-private package, so the SET + // of packages is itself published surface. A new package must be added + // to PUBLIC_PACKAGES here deliberately (or pinned as private below) — + // otherwise it would ship to npm without any pin applying to it. + expect(discoverManifestDirs()).toEqual([...Object.keys(PUBLIC_PACKAGES), 'core'].sort()); + }); +}); + +describe('internal packages stay private', () => { + test('@modelcontextprotocol/core is private (bundled into client/server dists)', () => { + const manifest = readManifest('core'); + expect(manifest.name).toBe('@modelcontextprotocol/core'); + expect(manifest.private).toBe(true); + }); + + test('the workspace root is private', () => { + const manifest = JSON.parse(readFileSync(join(packagesDir, '..', 'package.json'), 'utf8')) as PackageManifest; + expect(manifest.private).toBe(true); + }); +}); diff --git a/packages/core/test/shared/customMethods.test.ts b/packages/core/test/shared/customMethods.test.ts index ffee5b9a7d..b50c376793 100644 --- a/packages/core/test/shared/customMethods.test.ts +++ b/packages/core/test/shared/customMethods.test.ts @@ -42,7 +42,15 @@ describe('Protocol custom-method support', () => { expect(result.items).toEqual(['result for hello']); }); - it('strips _meta from params before validation', async () => { + it('passes _meta to custom-handler validation, minus the reserved envelope keys (deliberate flip)', async () => { + // BEHAVIOR MIGRATION (Q1 increment 2, ledgered): custom handlers + // used to have _meta DELETED before their params validation. They + // now receive it present-minus-reserved — the wire-only lift has + // already removed the io.modelcontextprotocol/* envelope keys — + // making the custom path consistent with the spec-method path. + // Strict consumer schemas that reject unknown keys must now model + // (or strip) _meta. Changeset: codec-split-wire-break; + // docs/migration.md "custom handlers receive _meta". const [a, b] = await pair(); const Strict = z.strictObject({ x: z.number() }); b.setRequestHandler('acme/strict', { params: Strict }, async params => { @@ -50,8 +58,20 @@ describe('Protocol custom-method support', () => { return {}; }); - const result = await a.request({ method: 'acme/strict', params: { x: 1, _meta: { progressToken: 't' } } }, z.object({})); - expect(result).toEqual({}); + // A strict schema now sees the metadata and rejects it… + await expect( + a.request({ method: 'acme/strict', params: { x: 1, _meta: { progressToken: 't' } } }, z.object({})) + ).rejects.toThrow(ProtocolError); + + // …while a schema that models _meta receives it verbatim. + const WithMeta = z.strictObject({ x: z.number(), _meta: z.record(z.string(), z.unknown()).optional() }); + let seenParams: unknown; + b.setRequestHandler('acme/withMeta', { params: WithMeta }, async params => { + seenParams = params; + return {}; + }); + await a.request({ method: 'acme/withMeta', params: { x: 2, _meta: { progressToken: 't' } } }, z.object({})); + expect(seenParams).toEqual({ x: 2, _meta: { progressToken: 't' } }); }); it('rejects invalid params with ProtocolError(InvalidParams)', async () => { @@ -112,17 +132,22 @@ describe('Protocol custom-method support', () => { expect(seen).toEqual([{ stage: 'fetch', pct: 0.5 }]); }); - it('passes the raw notification (with _meta) as the second handler argument', async () => { + it('passes _meta through custom-notification validation, minus reserved keys (deliberate flip)', async () => { + // Same behavior migration as the request path: _meta is no longer + // deleted before the consumer schema runs (ledgered; changeset: + // codec-split-wire-break). const [a, b] = await pair(); - const Strict = z.strictObject({ stage: z.string() }); + const WithMeta = z.strictObject({ stage: z.string(), _meta: z.record(z.string(), z.unknown()).optional() }); + let seenParams: unknown; let seenMeta: unknown; - b.setNotificationHandler('acme/searchProgress', { params: Strict }, (params, notification) => { - expect(params).toEqual({ stage: 'fetch' }); + b.setNotificationHandler('acme/searchProgress', { params: WithMeta }, (params, notification) => { + seenParams = params; seenMeta = notification.params?._meta; }); await a.notification({ method: 'acme/searchProgress', params: { stage: 'fetch', _meta: { traceId: 't1' } } }); await new Promise(r => setTimeout(r, 0)); + expect(seenParams).toEqual({ stage: 'fetch', _meta: { traceId: 't1' } }); expect(seenMeta).toEqual({ traceId: 't1' }); }); }); diff --git a/packages/core/test/shared/protocol.test.ts b/packages/core/test/shared/protocol.test.ts index 6e77430d61..309cf6a50a 100644 --- a/packages/core/test/shared/protocol.test.ts +++ b/packages/core/test/shared/protocol.test.ts @@ -22,6 +22,8 @@ import type { } from '../../src/types/index.js'; import { ProtocolError, ProtocolErrorCode } from '../../src/types/index.js'; import { SdkError, SdkErrorCode } from '../../src/errors/sdkErrors.js'; +import { InMemoryTransport } from '../../src/util/inMemory.js'; +import { rev2025Codec } from '../../src/wire/rev2025-11-25/codec.js'; // Test Protocol subclass for testing class TestProtocolImpl extends Protocol { @@ -910,3 +912,169 @@ describe('mergeCapabilities', () => { expect(merged).toEqual({}); }); }); + +describe('codec-seam hardening in the protocol funnels', () => { + afterEach(() => { + vi.restoreAllMocks(); + }); + + const flush = () => new Promise(resolve => setTimeout(resolve, 10)); + + test('a throw inside codec.encodeResult answers −32603 on the wire — the peer is never stranded', async () => { + const [peerTx, protocolTx] = InMemoryTransport.createLinkedPair(); + const sent: JSONRPCMessage[] = []; + peerTx.onmessage = message => void sent.push(message); + await peerTx.start(); + + const protocol = createTestProtocol(); + const errors: Error[] = []; + protocol.onerror = error => void errors.push(error); + protocol.setRequestHandler('acme/op', { params: z.looseObject({}) }, () => ({ ok: true }) as Result); + await protocol.connect(protocolTx); + + // The encode hop is the only throw-capable step between handler + // success and the transport send (and it grows stamping content in + // M3.2). Force it to throw once. + vi.spyOn(rev2025Codec, 'encodeResult').mockImplementationOnce(() => { + throw new Error('stamp exploded'); + }); + + await peerTx.send({ jsonrpc: '2.0', id: 1, method: 'acme/op', params: {} }); + await flush(); + + expect(sent).toHaveLength(1); + expect((sent[0] as JSONRPCErrorResponse).error).toMatchObject({ code: ProtocolErrorCode.InternalError }); + // Surfaced locally too. + expect(errors.some(error => error.message.includes('Failed to encode result'))).toBe(true); + + // The connection stays serviceable: the next request round-trips. + await peerTx.send({ jsonrpc: '2.0', id: 2, method: 'acme/op', params: {} }); + await flush(); + expect(sent).toHaveLength(2); + expect((sent[1] as JSONRPCResultResponse).result).toMatchObject({ ok: true }); + + await protocol.close(); + }); + + test('a synchronous throw out of codec.decodeResult rejects the request instead of escaping into transport.onmessage', async () => { + const [protocolTx, peerTx] = InMemoryTransport.createLinkedPair(); + peerTx.onmessage = message => { + const request = message as JSONRPCRequest; + void peerTx.send({ jsonrpc: '2.0', id: request.id, result: {} }); + }; + await peerTx.start(); + + const protocol = createTestProtocol(); + await protocol.connect(protocolTx); + + // The response callback runs synchronously inside _onresponse; an + // unguarded throw here would propagate into the transport instead of + // failing the request. (The concrete production vector is the 2026 + // codec's method-keyed schema lookup — see the own-key guard in + // rev2026-07-28/codec.ts.) + vi.spyOn(rev2025Codec, 'decodeResult').mockImplementationOnce(() => { + throw new Error('decode exploded'); + }); + + await expect(protocol.request({ method: 'ping' })).rejects.toThrow('decode exploded'); + + await protocol.close(); + }); +}); + +describe('inbound validation precedence: −32601 outranks envelope −32602', () => { + afterEach(() => { + vi.restoreAllMocks(); + }); + + const flush = () => new Promise(resolve => setTimeout(resolve, 10)); + + async function wireWithFailingEnvelope(setup?: (protocol: TestProtocolImpl) => void) { + const [peerTx, protocolTx] = InMemoryTransport.createLinkedPair(); + const sent: JSONRPCMessage[] = []; + peerTx.onmessage = message => void sent.push(message); + await peerTx.start(); + + const protocol = createTestProtocol(); + setup?.(protocol); + await protocol.connect(protocolTx); + + // Force the era's envelope check to fail for every request, so the + // test pins WHERE in the ladder it runs, independent of era wiring. + vi.spyOn(rev2025Codec, 'checkInboundEnvelope').mockImplementation(() => 'Request is missing the required _meta envelope'); + + return { peerTx, sent, flush }; + } + + test('a genuinely unknown method answers −32601 even when the envelope check would also fail', async () => { + const { peerTx, sent } = await wireWithFailingEnvelope(); + + await peerTx.send({ jsonrpc: '2.0', id: 1, method: 'acme/no-such-method', params: {} }); + await flush(); + + expect(sent).toHaveLength(1); + expect((sent[0] as JSONRPCErrorResponse).error).toMatchObject({ + code: ProtocolErrorCode.MethodNotFound, + message: 'Method not found' + }); + }); + + test('a served method still answers −32602 when the envelope check fails', async () => { + const { peerTx, sent } = await wireWithFailingEnvelope(protocol => { + protocol.setRequestHandler('acme/known', { params: z.looseObject({}) }, () => ({}) as Result); + }); + + await peerTx.send({ jsonrpc: '2.0', id: 1, method: 'acme/known', params: {} }); + await flush(); + + expect(sent).toHaveLength(1); + expect((sent[0] as JSONRPCErrorResponse).error).toMatchObject({ + code: ProtocolErrorCode.InvalidParams, + message: 'Request is missing the required _meta envelope' + }); + }); +}); + +describe('inbound protocol-version mismatch (−32004): the error data lists every supported version', () => { + const flush = () => new Promise(resolve => setTimeout(resolve, 10)); + + test('a request classified for a protocol version this connection does not serve is rejected with the full supported list', async () => { + const supportedProtocolVersions = ['2025-11-25', '2025-06-18', '2025-03-26']; + const [peerTx, protocolTx] = InMemoryTransport.createLinkedPair(); + const sent: JSONRPCMessage[] = []; + peerTx.onmessage = message => void sent.push(message); + await peerTx.start(); + + const protocol = new TestProtocolImpl({ supportedProtocolVersions }); + const errors: Error[] = []; + protocol.onerror = error => void errors.push(error); + await protocol.connect(protocolTx); + + // Deliver a request whose transport-edge classification names a + // protocol version this connection does not serve. The rejection's + // `data.supported` must list every protocol version the receiver + // supports — not just the version the connection is on — so the peer + // can pick a mutually supported version from the error alone. + protocolTx.onmessage?.( + { jsonrpc: '2.0', id: 1, method: 'tools/list', params: {} } as JSONRPCMessage, + // The in-memory transport's onmessage declares the narrower + // pre-classification extra type; the protocol layer reads the + // full MessageExtraInfo (same cast as the era-gate suite). + { classification: { era: 'modern' } } as never + ); + await flush(); + + expect(sent).toHaveLength(1); + const error = (sent[0] as JSONRPCErrorResponse).error as { + code: number; + message: string; + data?: { supported?: string[]; requested?: string }; + }; + expect(error.code).toBe(-32004); + expect(error.message).toContain('Unsupported protocol version'); + expect(error.data?.supported).toEqual(supportedProtocolVersions); + expect(error.data?.requested).toBe('2026-07-28'); + + await protocol.close(); + }); +}); diff --git a/packages/core/test/shared/rawResultTypeFirst.test.ts b/packages/core/test/shared/rawResultTypeFirst.test.ts new file mode 100644 index 0000000000..51fb40211f --- /dev/null +++ b/packages/core/test/shared/rawResultTypeFirst.test.ts @@ -0,0 +1,216 @@ +/** + * Raw-first result discrimination (V-1) — relocated to its structural home: + * step 1 of the era codec's `decodeResult`, BEFORE any schema validation + * (Q1 increment 2; previously a funnel insertion in `_requestWithSchema`). + * + * The postures are ERA-SCOPED (Q1-SD3): + * + * 2026 era (the connection negotiated '2026-07-28'): + * - `resultType` is REQUIRED. Absent → typed error NAMING the spec + * violation (the absent⇒complete bridge is scoped to earlier-revision + * servers and deliberately NOT extended to modern traffic). + * - `input_required` → discriminated driver payload, surfaced as a typed + * local error until the multi-round-trip driver (M4.1) consumes it. + * - unknown kinds → invalid, no retry. Non-string → invalid. + * - `'complete'` → wire-exact parse (resultType present) then lift. + * + * 2025 era (any legacy version / unbound instance): + * - `resultType` is FOREIGN vocabulary → strip-on-lift (tolerate-and-drop, + * whatever its value); validation then judges the actual content. This is + * a deliberate behavior migration from the era-blind funnel arm (ledgered; + * changeset: codec-split-wire-break). + * + * Either way, the V-1 invariant holds: a non-complete body can NEVER be + * masked into a hollow success by a tolerant result schema. + */ +import { describe, expect, test } from 'vitest'; + +import { SdkError, SdkErrorCode } from '../../src/errors/sdkErrors.js'; +import type { BaseContext } from '../../src/shared/protocol.js'; +import { Protocol, setNegotiatedProtocolVersion } from '../../src/shared/protocol.js'; +import type { JSONRPCRequest } from '../../src/types/index.js'; +import { InMemoryTransport } from '../../src/util/inMemory.js'; + +class TestProtocol extends Protocol { + protected assertCapabilityForMethod(): void {} + protected assertNotificationCapability(): void {} + protected assertRequestHandlerCapability(): void {} + protected buildContext(ctx: BaseContext): BaseContext { + return ctx; + } +} + +/** Wire a protocol whose peer answers every request with the given raw result body. */ +async function wireWithRawResult(rawResult: unknown, era?: '2026-07-28'): Promise { + const [clientTx, serverTx] = InMemoryTransport.createLinkedPair(); + serverTx.onmessage = message => { + const request = message as JSONRPCRequest; + void serverTx.send({ jsonrpc: '2.0', id: request.id, result: rawResult } as Parameters[0]); + }; + await serverTx.start(); + const protocol = new TestProtocol(); + await protocol.connect(clientTx); + if (era) setNegotiatedProtocolVersion(protocol, era); + return protocol; +} + +const INPUT_REQUIRED_BODY = { + resultType: 'input_required', + inputRequests: { 'elicit-1': { method: 'elicitation/create', params: { mode: 'form', message: 'Name?' } } }, + requestState: 'opaque' +}; + +async function settle(protocol: TestProtocol): Promise<{ resolved: unknown } | { rejected: unknown }> { + return protocol.request({ method: 'tools/call', params: { name: 'echo', arguments: {} } }).then( + result => ({ resolved: result as unknown }), + error => ({ rejected: error as unknown }) + ); +} + +describe('raw-first resultType discrimination — 2026 era (codec decode step 1)', () => { + test('an input_required body surfaces the discriminated kind, never an empty-content success', async () => { + const protocol = await wireWithRawResult(INPUT_REQUIRED_BODY, '2026-07-28'); + const outcome = await settle(protocol); + + expect('resolved' in outcome, 'must not resolve as a success').toBe(false); + const rejection = (outcome as { rejected: unknown }).rejected; + expect(rejection).toBeInstanceOf(SdkError); + const typed = rejection as SdkError; + expect(typed.code).toBe(SdkErrorCode.UnsupportedResultType); + expect(typed.data).toMatchObject({ resultType: 'input_required', method: 'tools/call' }); + + await protocol.close(); + }); + + test('an unrecognized resultType kind is invalid — surfaced, no retry', async () => { + const protocol = await wireWithRawResult({ resultType: 'mystery-kind', content: [] }, '2026-07-28'); + const outcome = await settle(protocol); + + expect('rejected' in outcome).toBe(true); + const rejection = (outcome as { rejected: unknown }).rejected as SdkError; + expect(rejection).toBeInstanceOf(SdkError); + expect(rejection.code).toBe(SdkErrorCode.UnsupportedResultType); + expect(rejection.data).toMatchObject({ resultType: 'mystery-kind' }); + + await protocol.close(); + }); + + test('ABSENT resultType is a spec violation on the modern leg — typed error naming it (Q1-SD3 i)', async () => { + // The absent⇒complete bridge is scoped to earlier-revision servers; + // a 2026-negotiated peer that omits the REQUIRED member is broken. + const protocol = await wireWithRawResult({ content: [{ type: 'text', text: 'looks fine' }] }, '2026-07-28'); + const outcome = await settle(protocol); + + expect('rejected' in outcome).toBe(true); + const rejection = (outcome as { rejected: unknown }).rejected as SdkError; + expect(rejection).toBeInstanceOf(SdkError); + expect(rejection.code).toBe(SdkErrorCode.InvalidResult); + expect(rejection.message).toContain('missing required resultType'); + expect(rejection.data).toMatchObject({ method: 'tools/call', violation: 'missing-resultType' }); + + await protocol.close(); + }); + + test('a non-string resultType can never surface as a success', async () => { + const protocol = await wireWithRawResult({ resultType: 42, content: [] }, '2026-07-28'); + const outcome = await settle(protocol); + + expect('rejected' in outcome).toBe(true); + const rejection = (outcome as { rejected: unknown }).rejected as SdkError; + expect(rejection).toBeInstanceOf(SdkError); + expect(rejection.code).toBe(SdkErrorCode.InvalidResult); + expect(rejection.data).toMatchObject({ resultType: 42 }); + + await protocol.close(); + }); + + test("resultType 'complete' is consumed: the result resolves without the wire member", async () => { + const protocol = await wireWithRawResult({ resultType: 'complete', content: [{ type: 'text', text: 'done' }] }, '2026-07-28'); + + const result = await protocol.request({ method: 'tools/call', params: { name: 'echo', arguments: {} } }); + expect(result.content).toEqual([{ type: 'text', text: 'done' }]); + expect('resultType' in result).toBe(false); + + await protocol.close(); + }); +}); + +describe('raw-first resultType handling — 2025 era (strip-on-lift, Q1-SD3 ii)', () => { + test('a foreign input_required body is stripped, then validation judges the content — never a silent success', async () => { + // BEHAVIOR MIGRATION (ledgered): pre-split, the era-blind funnel arm + // rejected this with UnsupportedResultType on every leg. On the 2025 + // era resultType carries no meaning — the ruled posture strips the + // foreign key and lets validation decide. The body has no content, + // so it fails the (default-free) tools/call result schema LOUDLY — + // the V-1 invariant (never a hollow success) holds. + const protocol = await wireWithRawResult(INPUT_REQUIRED_BODY); + const outcome = await settle(protocol); + + expect('resolved' in outcome, 'must not resolve as a success').toBe(false); + const rejection = (outcome as { rejected: unknown }).rejected as SdkError; + expect(rejection).toBeInstanceOf(SdkError); + expect(rejection.code).toBe(SdkErrorCode.InvalidResult); + + await protocol.close(); + }); + + test('strip-on-lift is VALUE-BLIND: a foreign input_required WITH a valid body resolves, member stripped', async () => { + // The strip keys on the member's PRESENCE, never its value — even the + // driver kind is foreign vocabulary on this era. With a valid body + // the request resolves; the stripped key never surfaces. (The + // sibling test above covers the invalid-body arm: there the strip + // also runs, and validation then rejects on the actual content.) + const protocol = await wireWithRawResult({ resultType: 'input_required', content: [{ type: 'text', text: 'ok' }] }); + + const result = await protocol.request({ method: 'tools/call', params: { name: 'echo', arguments: {} } }); + expect(result.content).toEqual([{ type: 'text', text: 'ok' }]); + expect('resultType' in result).toBe(false); + + await protocol.close(); + }); + + test('a foreign non-string resultType is stripped; an otherwise-valid result resolves without it', async () => { + const protocol = await wireWithRawResult({ resultType: 42, content: [{ type: 'text', text: 'ok' }] }); + + const result = await protocol.request({ method: 'tools/call', params: { name: 'echo', arguments: {} } }); + expect(result.content).toEqual([{ type: 'text', text: 'ok' }]); + expect('resultType' in result).toBe(false); + + await protocol.close(); + }); + + test("resultType 'complete' on a strict empty result still parses (stripped before validation)", async () => { + const protocol = await wireWithRawResult({ resultType: 'complete' }); + + const result = await protocol.request({ method: 'ping' }); + expect(result).toEqual({}); + + await protocol.close(); + }); + + test('absent resultType is untouched 2025-era behavior (siblings kept)', async () => { + const protocol = await wireWithRawResult({ content: [{ type: 'text', text: 'plain' }], extraSibling: 'kept' }); + + const result = await protocol.request({ method: 'tools/call', params: { name: 'echo', arguments: {} } }); + expect(result.content).toEqual([{ type: 'text', text: 'plain' }]); + expect((result as Record).extraSibling).toBe('kept'); + + await protocol.close(); + }); +}); + +describe('decode step 2 — the wire-exact schema lookup is own-key only', () => { + test("a prototype-chain method name (e.g. 'constructor') skips the wire-exact parse instead of throwing", async () => { + const { rev2026Codec } = await import('../../src/wire/rev2026-07-28/codec.js'); + // A bare object-prototype hit would surface Function (not a schema) + // and throw a TypeError out of the decode hop. The lookup must treat + // non-own keys exactly like unknown methods: no wire-exact parse, + // straight to the lift. + const decoded = rev2026Codec.decodeResult('constructor', { resultType: 'complete', anything: 'kept' }); + expect(decoded.kind).toBe('complete'); + if (decoded.kind === 'complete') { + expect((decoded.result as Record).anything).toBe('kept'); + expect('resultType' in decoded.result).toBe(false); + } + }); +}); diff --git a/packages/core/test/shared/typedMapAlignment.test.ts b/packages/core/test/shared/typedMapAlignment.test.ts new file mode 100644 index 0000000000..1cd836d3db --- /dev/null +++ b/packages/core/test/shared/typedMapAlignment.test.ts @@ -0,0 +1,139 @@ +/** + * Runtime/typed result-map alignment. + * + * `getResultSchema`'s typed overload asserts `z.ZodType`, + * so the runtime map must not be looser than the typed map: no task-result + * union members on `tools/call` / `sampling/createMessage` / + * `elicitation/create` (ResultTypeMap types them plain), and no `tasks/*` + * entries at all (the task methods are 2025-11-25 wire vocabulary outside + * `RequestMethod`). + * + * The behavioral consequence for a generic `request()` caller facing a + * 2025-era task server: a `CreateTaskResult` body can no longer parse via a + * union member and surface mis-typed (a `CreateTaskResult` typed as + * `CreateMessageResult`/`ElicitResult`). Where the method's result schema + * rejects the body it now fails as a typed invalid-result error. This client + * cannot drive tasks; a typed error is the correct surface, not a result + * whose static type lies. + */ +import { describe, expect, test } from 'vitest'; + +import { SdkError, SdkErrorCode } from '../../src/errors/sdkErrors.js'; +import type { BaseContext } from '../../src/shared/protocol.js'; +import { Protocol } from '../../src/shared/protocol.js'; +import { InMemoryTransport } from '../../src/util/inMemory.js'; +import type { JSONRPCRequest } from '../../src/types/index.js'; +// Post-relocation home (Q1 increment-2 step 1): the runtime registries live +// behind the per-era wire-codec interface now. +import { getResultSchema } from '../../src/wire/rev2025-11-25/registry.js'; + +class TestProtocol extends Protocol { + protected assertCapabilityForMethod(): void {} + protected assertNotificationCapability(): void {} + protected assertRequestHandlerCapability(): void {} + protected buildContext(ctx: BaseContext): BaseContext { + return ctx; + } +} + +/** A well-formed 2025-11-25 `CreateTaskResult` body. */ +const CREATE_TASK_RESULT_BODY = { + task: { + taskId: 'task-1', + status: 'working', + ttl: 60_000, + createdAt: '2025-11-25T00:00:00Z', + lastUpdatedAt: '2025-11-25T00:00:00Z', + pollInterval: 500 + } +}; + +/** Wire a protocol whose peer answers every request with the given raw result body. */ +async function wireWithRawResult(rawResult: unknown): Promise { + const [clientTx, serverTx] = InMemoryTransport.createLinkedPair(); + serverTx.onmessage = message => { + const request = message as JSONRPCRequest; + void serverTx.send({ jsonrpc: '2.0', id: request.id, result: rawResult } as Parameters[0]); + }; + await serverTx.start(); + const protocol = new TestProtocol(); + await protocol.connect(clientTx); + return protocol; +} + +describe('task-shaped result bodies against the narrowed runtime map', () => { + test('sampling/createMessage: a CreateTaskResult body is a typed invalid-result error, not a mis-typed success', async () => { + // Before the narrowing, the union member parsed this body and handed + // it back TYPED as CreateMessageResult — a result whose static type + // lies. Now it fails the (plain) result schema locally. + const protocol = await wireWithRawResult(CREATE_TASK_RESULT_BODY); + + const outcome = await protocol.request({ method: 'sampling/createMessage', params: { messages: [], maxTokens: 1 } }).then( + result => ({ resolved: result as unknown }), + error => ({ rejected: error as unknown }) + ); + + expect('resolved' in outcome, 'must not resolve as a success').toBe(false); + const rejection = (outcome as { rejected: unknown }).rejected; + expect(rejection).toBeInstanceOf(SdkError); + expect((rejection as SdkError).code).toBe(SdkErrorCode.InvalidResult); + + await protocol.close(); + }); + + test('elicitation/create: a CreateTaskResult body is a typed invalid-result error, not a mis-typed success', async () => { + const protocol = await wireWithRawResult(CREATE_TASK_RESULT_BODY); + + const rejection = await protocol + .request({ method: 'elicitation/create', params: { mode: 'form', message: 'Name?', requestedSchema: { type: 'object' } } }) + .catch((error: unknown) => error); + expect(rejection).toBeInstanceOf(SdkError); + expect((rejection as SdkError).code).toBe(SdkErrorCode.InvalidResult); + + await protocol.close(); + }); + + test('tools/call: a CreateTaskResult body is now a typed invalid-result error too (content-default removal flip)', async () => { + // FLIPPED PIN (Q1 increment 2, ledgered with the content-default + // removal — changeset: codec-split-wire-break). The previous "Honest + // pin, not an endorsement" recorded that CallToolResultSchema's + // content.default([]) swallowed ANY object — including a task body — + // as a content-empty success, which made the old union member + // unreachable and the map narrowing observationally invisible for + // tools/call. With `content` now REQUIRED at the wire boundary the + // masking surface is gone: a task body has no `content`, fails the + // plain schema, and surfaces as the same typed invalid-result error + // as sampling/elicit. The result-schema-strictness question the old + // pin deferred is hereby resolved: loud rejection. + const protocol = await wireWithRawResult(CREATE_TASK_RESULT_BODY); + + const rejection = await protocol + .request({ method: 'tools/call', params: { name: 'echo', arguments: {} } }) + .catch((error: unknown) => error); + expect(rejection).toBeInstanceOf(SdkError); + expect((rejection as SdkError).code).toBe(SdkErrorCode.InvalidResult); + + await protocol.close(); + }); +}); + +describe('tasks/* entries are gone from the runtime result map', () => { + test('getResultSchema returns undefined for every task method', () => { + for (const method of ['tasks/get', 'tasks/result', 'tasks/list', 'tasks/cancel']) { + expect(getResultSchema(method), method).toBeUndefined(); + } + }); + + test('a generic request() for a task method demands an explicit schema', async () => { + // The typed overload already excluded task methods; the runtime map + // entries were typed-unreachable leftovers. Without them, the + // explicit-schema overload is the one (intentional) interop path. + const protocol = await wireWithRawResult({}); + + expect(() => protocol.request({ method: 'tasks/get', params: { taskId: 't-1' } } as never)).toThrow( + /'tasks\/get' is not a spec method; pass a result schema/ + ); + + await protocol.close(); + }); +}); diff --git a/packages/core/test/shared/wireOnlyLift.test.ts b/packages/core/test/shared/wireOnlyLift.test.ts new file mode 100644 index 0000000000..32a8eb5302 --- /dev/null +++ b/packages/core/test/shared/wireOnlyLift.test.ts @@ -0,0 +1,329 @@ +/** + * Envelope lift, two-sided: wire-only material is hidden from handlers AND + * (for requests) reaches the protocol layer un-deleted. + * + * Hide set, per message kind. Requests: the reserved + * `io.modelcontextprotocol/*` envelope `_meta` keys and the multi-round-trip + * retry fields (`inputResponses`/`requestState`) — the envelope is readable + * via `ctx.mcpReq.envelope` and the retry fields via + * `ctx.mcpReq.inputResponses`/`.requestState`. Notifications: ONLY the + * envelope `_meta` keys (the spec reserves the retry params names on + * client-initiated requests, not notifications), and there is no + * per-notification ctx, so the lifted envelope keys are dropped rather than + * surfaced. Under 2026-era traffic, handler params must be byte-equal to the + * 2025-era shape of the same call; traffic without wire-only material passes + * through untouched (same reference — no cloning on the hot path). + */ +import { describe, expect, expectTypeOf, test } from 'vitest'; +import * as z from 'zod/v4'; + +import type { BaseContext } from '../../src/shared/protocol.js'; +import { Protocol } from '../../src/shared/protocol.js'; +import { InMemoryTransport } from '../../src/util/inMemory.js'; +import type { JSONRPCMessage, JSONRPCRequest, RequestMetaEnvelope, Result } from '../../src/types/index.js'; +import { + CLIENT_CAPABILITIES_META_KEY, + CLIENT_INFO_META_KEY, + LOG_LEVEL_META_KEY, + PROTOCOL_VERSION_META_KEY, + RELATED_TASK_META_KEY +} from '../../src/types/index.js'; + +class TestProtocol extends Protocol { + protected assertCapabilityForMethod(): void {} + protected assertNotificationCapability(): void {} + protected assertRequestHandlerCapability(): void {} + protected buildContext(ctx: BaseContext): BaseContext { + return ctx; + } +} + +const ENVELOPE = { + [PROTOCOL_VERSION_META_KEY]: '2026-07-28', + [CLIENT_INFO_META_KEY]: { name: 'modern-client', version: '1.0.0' }, + [CLIENT_CAPABILITIES_META_KEY]: { elicitation: {} }, + [LOG_LEVEL_META_KEY]: 'info' +}; + +interface Wired { + receiver: TestProtocol; + peer: InMemoryTransport; + responses: JSONRPCMessage[]; +} + +async function wireReceiver(setup: (receiver: TestProtocol) => void): Promise { + const [peer, receiverTx] = InMemoryTransport.createLinkedPair(); + const receiver = new TestProtocol(); + setup(receiver); + await receiver.connect(receiverTx); + const responses: JSONRPCMessage[] = []; + peer.onmessage = message => void responses.push(message); + await peer.start(); + return { receiver, peer, responses }; +} + +const flush = () => new Promise(resolve => setTimeout(resolve, 20)); + +describe('envelope lift on inbound requests', () => { + test('handler params are byte-equal to the 2025 shape; envelope readable via ctx', async () => { + let seenRequest: unknown; + let seenCtx: BaseContext | undefined; + const { peer } = await wireReceiver(receiver => { + receiver.setRequestHandler('tools/call', (request, ctx) => { + seenRequest = request; + seenCtx = ctx; + return { content: [] }; + }); + }); + + // A modern request: envelope keys ride _meta next to 2025-legal + // material (progressToken, related-task). + await peer.send({ + jsonrpc: '2.0', + id: 1, + method: 'tools/call', + params: { + name: 'echo', + arguments: { text: 'hi' }, + _meta: { ...ENVELOPE, progressToken: 7, [RELATED_TASK_META_KEY]: { taskId: 't-1' } } + } + } as JSONRPCMessage); + await flush(); + + // Byte-equal to the 2025-era shape of the same call (the spec-method + // handler receives the schema-parsed {method, params} form). + expect(seenRequest).toEqual({ + method: 'tools/call', + params: { + name: 'echo', + arguments: { text: 'hi' }, + _meta: { progressToken: 7, [RELATED_TASK_META_KEY]: { taskId: 't-1' } } + } + }); + // ctx._meta mirrors the lifted _meta… + expect(seenCtx?.mcpReq._meta).toEqual({ progressToken: 7, [RELATED_TASK_META_KEY]: { taskId: 't-1' } }); + // …and the envelope is surfaced verbatim, un-deleted. + expect(seenCtx?.mcpReq.envelope).toEqual(ENVELOPE); + }); + + test('a partial envelope (a subset of the reserved keys) surfaces as received and types as Partial', async () => { + // A one-revision-old peer may legally send only some reserved keys + // (e.g. just the log-level opt-in). The lift surfaces whatever was + // present, and the ctx slot's type says so: every member is optional. + let seenCtx: BaseContext | undefined; + const { peer } = await wireReceiver(receiver => { + receiver.setRequestHandler('tools/call', (_request, ctx) => { + seenCtx = ctx; + return { content: [] }; + }); + }); + + await peer.send({ + jsonrpc: '2.0', + id: 7, + method: 'tools/call', + params: { name: 'echo', arguments: {}, _meta: { [LOG_LEVEL_META_KEY]: 'debug' } } + } as JSONRPCMessage); + await flush(); + + expect(seenCtx?.mcpReq.envelope).toEqual({ [LOG_LEVEL_META_KEY]: 'debug' }); + // The slot is Partial: a key the request did not + // carry reads as possibly-undefined — there is no claim that the + // required envelope members exist (requiredness is enforced per + // request at dispatch time, not by the lift). + expectTypeOf>().toEqualTypeOf>(); + expectTypeOf(seenCtx!.mcpReq.envelope![PROTOCOL_VERSION_META_KEY]).toEqualTypeOf(); + expect(seenCtx?.mcpReq.envelope?.[PROTOCOL_VERSION_META_KEY]).toBeUndefined(); + }); + + test('a _meta that holds only envelope keys disappears entirely (exact 2025 shape)', async () => { + let seenRequest: unknown; + const { peer } = await wireReceiver(receiver => { + receiver.setRequestHandler('tools/call', request => { + seenRequest = request; + return { content: [] }; + }); + }); + + await peer.send({ + jsonrpc: '2.0', + id: 2, + method: 'tools/call', + params: { name: 'echo', arguments: {}, _meta: { ...ENVELOPE } } + } as JSONRPCMessage); + await flush(); + + expect(seenRequest).toEqual({ + method: 'tools/call', + params: { name: 'echo', arguments: {} } + }); + }); + + test('retry fields are hidden from handler params and reach ctx un-deleted', async () => { + let seenRequest: unknown; + let seenCtx: BaseContext | undefined; + const { peer } = await wireReceiver(receiver => { + receiver.setRequestHandler('tools/call', (request, ctx) => { + seenRequest = request; + seenCtx = ctx; + return { content: [] }; + }); + }); + + const inputResponses = { 'req-1': { action: 'accept', content: { name: 'octocat' } } }; + await peer.send({ + jsonrpc: '2.0', + id: 3, + method: 'tools/call', + params: { name: 'echo', arguments: {}, inputResponses, requestState: 'opaque-state-token' } + } as JSONRPCMessage); + await flush(); + + expect(seenRequest).toEqual({ + method: 'tools/call', + params: { name: 'echo', arguments: {} } + }); + expect(seenCtx?.mcpReq.inputResponses).toEqual(inputResponses); + expect(seenCtx?.mcpReq.requestState).toBe('opaque-state-token'); + }); + + test('the custom-method (3-arg) path also surfaces the envelope via ctx', async () => { + let seenParams: unknown; + let seenCtx: BaseContext | undefined; + const { peer, responses } = await wireReceiver(receiver => { + receiver.setRequestHandler('acme/search', { params: z.object({ query: z.string() }) }, (params, ctx) => { + seenParams = params; + seenCtx = ctx; + return { hits: [] }; + }); + }); + + await peer.send({ + jsonrpc: '2.0', + id: 4, + method: 'acme/search', + params: { query: 'mcp', _meta: { ...ENVELOPE } } + } as JSONRPCMessage); + await flush(); + + expect(seenParams).toEqual({ query: 'mcp' }); + expect(seenCtx?.mcpReq.envelope).toEqual(ENVELOPE); + expect(responses).toHaveLength(1); + }); + + test('the fallback request handler receives the lifted request too', async () => { + let seenRequest: JSONRPCRequest | undefined; + const { peer } = await wireReceiver(receiver => { + receiver.fallbackRequestHandler = request => { + seenRequest = request; + return Promise.resolve({} as Result); + }; + }); + + await peer.send({ + jsonrpc: '2.0', + id: 5, + method: 'vendor/anything', + params: { value: 1, _meta: { ...ENVELOPE }, requestState: 's' } + } as JSONRPCMessage); + await flush(); + + expect(seenRequest?.params).toEqual({ value: 1 }); + }); + + test('2025-era requests pass through untouched (same reference, no ctx slots)', async () => { + let seenRequest: JSONRPCRequest | undefined; + let seenCtx: BaseContext | undefined; + const { peer } = await wireReceiver(receiver => { + receiver.fallbackRequestHandler = (request, ctx) => { + seenRequest = request; + seenCtx = ctx; + return Promise.resolve({} as Result); + }; + }); + + const legacy = { + jsonrpc: '2.0', + id: 6, + method: 'vendor/legacy', + params: { value: 2, _meta: { progressToken: 9 } } + } as JSONRPCMessage; + await peer.send(legacy); + await flush(); + + // Identity preserved: the lift allocates nothing for clean traffic. + expect(seenRequest).toBe(legacy); + expect(seenCtx?.mcpReq.envelope).toBeUndefined(); + expect(seenCtx?.mcpReq.inputResponses).toBeUndefined(); + expect(seenCtx?.mcpReq.requestState).toBeUndefined(); + }); +}); + +describe('envelope lift on inbound notifications', () => { + test('notification handlers never see the reserved envelope keys', async () => { + let seenParams: unknown; + let seenNotification: unknown; + const { peer } = await wireReceiver(receiver => { + receiver.setNotificationHandler('vendor/changed', { params: z.object({ value: z.number() }) }, (params, notification) => { + seenParams = params; + seenNotification = notification; + }); + }); + + await peer.send({ + jsonrpc: '2.0', + method: 'vendor/changed', + params: { value: 42, _meta: { ...ENVELOPE, progressToken: 1 } } + } as JSONRPCMessage); + await flush(); + + expect(seenParams).toEqual({ value: 42 }); + // The raw notification handed to the handler is the lifted one: + // _meta retains only non-reserved material. + expect((seenNotification as { params?: { _meta?: unknown } }).params?._meta).toEqual({ progressToken: 1 }); + }); + + test('top-level params named like the retry fields reach notification handlers intact', async () => { + // The spec reserves `inputResponses`/`requestState` on + // client-initiated REQUESTS only. A vendor notification is free to + // use those names as ordinary params — the lift must not touch them + // (notifications have no ctx, so a delete would be unrecoverable). + let seenParams: unknown; + const { peer } = await wireReceiver(receiver => { + receiver.setNotificationHandler( + 'vendor/stateChanged', + { params: z.looseObject({ requestState: z.string() }) }, + params => void (seenParams = params) + ); + }); + + await peer.send({ + jsonrpc: '2.0', + method: 'vendor/stateChanged', + params: { requestState: 'app-domain-value', inputResponses: { poll: 'yes' }, _meta: { ...ENVELOPE } } + } as JSONRPCMessage); + await flush(); + + // Envelope keys lifted; the retry-named top-level params untouched. + expect(seenParams).toEqual({ requestState: 'app-domain-value', inputResponses: { poll: 'yes' } }); + }); + + test('the fallback notification handler receives the lifted notification', async () => { + let seen: unknown; + const { peer } = await wireReceiver(receiver => { + receiver.fallbackNotificationHandler = notification => { + seen = notification; + return Promise.resolve(); + }; + }); + + await peer.send({ + jsonrpc: '2.0', + method: 'vendor/ping', + params: { _meta: { ...ENVELOPE } } + } as JSONRPCMessage); + await flush(); + + expect((seen as { params?: unknown }).params).toEqual({}); + }); +}); diff --git a/packages/core/test/spec.types.2025-11-25.test.ts b/packages/core/test/spec.types.2025-11-25.test.ts index bf0903cd1a..76d6d7bfab 100644 --- a/packages/core/test/spec.types.2025-11-25.test.ts +++ b/packages/core/test/spec.types.2025-11-25.test.ts @@ -1,19 +1,50 @@ /** - * Compares the SDK's types against the frozen 2025-11-25 release schema - * (spec.types.2025-11-25.ts). The 2026-07-28 comparison lives in + * Per-revision parity against the FROZEN 2025-11-25 release schema + * (spec.types.2025-11-25.ts). The draft comparison lives in * spec.types.2026-07-28.test.ts. * - * This contains: - * - Static type checks to verify the Spec's types are compatible with the SDK's types - * (mutually assignable — no type-level workarounds should be needed) - * - Runtime checks to verify each Spec type has a static check - * (note: a few don't have SDK types, see MISSING_SDK_TYPES below) + * Q1 increment 2 retired the 20 `@ts-expect-error` affordances this file + * used to carry: where the neutral public types deliberately follow the + * 2026-07-28 typing (the shared-tier adjudications), the comparisons now + * target the 2025-era WIRE-VIEW types (`wire/rev2025-11-25/wireTypes.ts`), + * which restate the anchor shape exactly and document each adjudication in + * one place. Zero affordances remain: every check below is exact, both + * directions, and the key-parity pins include the previously-suppressed + * names (PromptArgument/PromptReference `title`, the capabilities key sets). */ import fs from 'node:fs'; import path from 'node:path'; import type * as SpecTypes from '../src/types/spec.types.2025-11-25.js'; import type * as SDKTypes from '../src/types/index.js'; +// The era-faithful 2025 wire role unions (Q1 increment 2): the NEUTRAL role +// aggregates no longer carry task vocabulary — the 2025-era wire module does. +// Role-union comparisons against this FROZEN revision's anchor therefore +// target the wire-era artifacts. +import type * as Wire2025 from '../src/wire/rev2025-11-25/schemas.js'; +import type { + Wire2025ClientCapabilities, + Wire2025ClientRequestView, + Wire2025CreateMessageRequest, + Wire2025CreateMessageRequestParams, + Wire2025InitializeRequest, + Wire2025InitializeRequestParams, + Wire2025InitializeResult, + Wire2025ListToolsResult, + Wire2025PromptArgument, + Wire2025PromptReference, + Wire2025ServerCapabilities, + Wire2025ServerRequestView, + Wire2025Tool +} from '../src/wire/rev2025-11-25/wireTypes.js'; +import type * as z4 from 'zod/v4'; + +type Wire2025ClientRequest = z4.infer; +type Wire2025ClientNotification = z4.infer; +type Wire2025ClientResult = z4.infer; +type Wire2025ServerRequest = z4.infer; +type Wire2025ServerNotification = z4.infer; +type Wire2025ServerResult = z4.infer; /* eslint-disable @typescript-eslint/no-unused-vars */ @@ -36,8 +67,7 @@ const sdkTypeChecks = { sdk = spec; spec = sdk; }, - InitializeRequestParams: (sdk: SDKTypes.InitializeRequestParams, spec: SpecTypes.InitializeRequestParams) => { - // @ts-expect-error 2025-11-25 types capabilities.experimental values as `object`; the SDK follows the 2026-07-28 schema's JSONObject + InitializeRequestParams: (sdk: Wire2025InitializeRequestParams, spec: SpecTypes.InitializeRequestParams) => { sdk = spec; spec = sdk; }, @@ -87,10 +117,8 @@ const sdkTypeChecks = { sdk = spec; spec = sdk; }, - CreateMessageRequestParams: (sdk: SDKTypes.CreateMessageRequestParams, spec: SpecTypes.CreateMessageRequestParams) => { - // @ts-expect-error 2025-11-25 types `metadata` as `object`; the SDK follows the 2026-07-28 schema's JSONObject + CreateMessageRequestParams: (sdk: Wire2025CreateMessageRequestParams, spec: SpecTypes.CreateMessageRequestParams) => { sdk = spec; - // @ts-expect-error the SDK's JSONValue-typed tool inputSchema properties are not assignable to 2025-11-25's `object` spec = sdk; }, CompleteRequestParams: (sdk: SDKTypes.CompleteRequestParams, spec: SpecTypes.CompleteRequestParams) => { @@ -220,15 +248,15 @@ const sdkTypeChecks = { sdk = spec; spec = sdk; }, - ClientResult: (sdk: SDKTypes.ClientResult, spec: SpecTypes.ClientResult) => { + ClientResult: (sdk: Wire2025ClientResult, spec: SpecTypes.ClientResult) => { sdk = spec; spec = sdk; }, - ClientNotification: (sdk: WithJSONRPC, spec: SpecTypes.ClientNotification) => { + ClientNotification: (sdk: WithJSONRPC, spec: SpecTypes.ClientNotification) => { sdk = spec; spec = sdk; }, - ServerResult: (sdk: SDKTypes.ServerResult, spec: SpecTypes.ServerResult) => { + ServerResult: (sdk: Wire2025ServerResult, spec: SpecTypes.ServerResult) => { sdk = spec; spec = sdk; }, @@ -244,20 +272,16 @@ const sdkTypeChecks = { sdk = spec; spec = sdk; }, - Tool: (sdk: SDKTypes.Tool, spec: SpecTypes.Tool) => { - // @ts-expect-error 2025-11-25 types inputSchema/outputSchema properties as `object`; the SDK follows the 2026-07-28 schema's JSONValue + Tool: (sdk: Wire2025Tool, spec: SpecTypes.Tool) => { sdk = spec; - // @ts-expect-error the SDK's JSONValue-typed inputSchema/outputSchema properties are not assignable to 2025-11-25's `object` spec = sdk; }, ListToolsRequest: (sdk: WithJSONRPCRequest, spec: SpecTypes.ListToolsRequest) => { sdk = spec; spec = sdk; }, - ListToolsResult: (sdk: SDKTypes.ListToolsResult, spec: SpecTypes.ListToolsResult) => { - // @ts-expect-error 2025-11-25 vs 2026-07-28 Tool typing; see the Tool check above + ListToolsResult: (sdk: Wire2025ListToolsResult, spec: SpecTypes.ListToolsResult) => { sdk = spec; - // @ts-expect-error 2025-11-25 vs 2026-07-28 Tool typing; see the Tool check above spec = sdk; }, CallToolResult: (sdk: SDKTypes.CallToolResult, spec: SpecTypes.CallToolResult) => { @@ -476,48 +500,39 @@ const sdkTypeChecks = { sdk = spec; spec = sdk; }, - CreateMessageRequest: (sdk: WithJSONRPCRequest, spec: SpecTypes.CreateMessageRequest) => { - // @ts-expect-error 2025-11-25 vs 2026-07-28 typing of params metadata/tools; see the CreateMessageRequestParams check above + CreateMessageRequest: (sdk: WithJSONRPCRequest, spec: SpecTypes.CreateMessageRequest) => { sdk = spec; - // @ts-expect-error 2025-11-25 vs 2026-07-28 typing of params metadata/tools; see the CreateMessageRequestParams check above spec = sdk; }, - InitializeRequest: (sdk: WithJSONRPCRequest, spec: SpecTypes.InitializeRequest) => { - // @ts-expect-error 2025-11-25 types capabilities.experimental values as `object`; the SDK follows the 2026-07-28 schema's JSONObject + InitializeRequest: (sdk: WithJSONRPCRequest, spec: SpecTypes.InitializeRequest) => { sdk = spec; spec = sdk; }, - InitializeResult: (sdk: SDKTypes.InitializeResult, spec: SpecTypes.InitializeResult) => { - // @ts-expect-error 2025-11-25 types capabilities.experimental values as `object`; the SDK follows the 2026-07-28 schema's JSONObject + InitializeResult: (sdk: Wire2025InitializeResult, spec: SpecTypes.InitializeResult) => { sdk = spec; spec = sdk; }, - ClientCapabilities: (sdk: SDKTypes.ClientCapabilities, spec: SpecTypes.ClientCapabilities) => { - // @ts-expect-error 2025-11-25 types experimental/sampling/elicitation/tasks blobs as `object`; the SDK follows the 2026-07-28 schema's JSONObject + ClientCapabilities: (sdk: Wire2025ClientCapabilities, spec: SpecTypes.ClientCapabilities) => { sdk = spec; spec = sdk; }, - ServerCapabilities: (sdk: SDKTypes.ServerCapabilities, spec: SpecTypes.ServerCapabilities) => { - // @ts-expect-error 2025-11-25 types experimental/logging/completions/tasks blobs as `object`; the SDK follows the 2026-07-28 schema's JSONObject + ServerCapabilities: (sdk: Wire2025ServerCapabilities, spec: SpecTypes.ServerCapabilities) => { sdk = spec; spec = sdk; }, - ClientRequest: (sdk: WithJSONRPCRequest, spec: SpecTypes.ClientRequest) => { - // @ts-expect-error 2025-11-25 types capabilities.experimental values as `object` (via the InitializeRequest member); the SDK follows the 2026-07-28 schema's JSONObject + ClientRequest: (sdk: WithJSONRPCRequest, spec: SpecTypes.ClientRequest) => { sdk = spec; spec = sdk; }, - ServerRequest: (sdk: WithJSONRPCRequest, spec: SpecTypes.ServerRequest) => { - // @ts-expect-error 2025-11-25 vs 2026-07-28 typing of CreateMessageRequest params; see the CreateMessageRequestParams check above + ServerRequest: (sdk: WithJSONRPCRequest, spec: SpecTypes.ServerRequest) => { sdk = spec; - // @ts-expect-error 2025-11-25 vs 2026-07-28 typing of CreateMessageRequest params; see the CreateMessageRequestParams check above spec = sdk; }, LoggingMessageNotification: (sdk: WithJSONRPC, spec: SpecTypes.LoggingMessageNotification) => { sdk = spec; spec = sdk; }, - ServerNotification: (sdk: WithJSONRPC, spec: SpecTypes.ServerNotification) => { + ServerNotification: (sdk: WithJSONRPC, spec: SpecTypes.ServerNotification) => { sdk = spec; spec = sdk; }, @@ -662,14 +677,6 @@ type AssertExactKeys< /** Constraint: T must resolve to `true`. */ type Assert = T; -/** - * Same as {@link AssertExactKeys}, but tolerates the SDK's `resultType` key on - * result shapes: the SDK follows the 2026-07-28 schema's optional `resultType` - * passthrough (absent means "complete"), which is not in released 2025-11-25. - * Every other key still has to match exactly. - */ -type AssertExactKeysWithResultType = AssertExactKeys; - /* * Excluded from key-level assertions (21 entries): * @@ -710,38 +717,34 @@ type _K_ElicitRequestURLParams = Assert>; type _K_BaseMetadata = Assert>; type _K_Implementation = Assert>; -type _K_PaginatedResult = Assert>; -type _K_ListRootsResult = Assert>; +type _K_PaginatedResult = Assert>; +type _K_ListRootsResult = Assert>; type _K_Root = Assert>; -type _K_ElicitResult = Assert>; -type _K_CompleteResult = Assert>; +type _K_ElicitResult = Assert>; +type _K_CompleteResult = Assert>; type _K_Request = Assert>; -type _K_Result = Assert>; +type _K_Result = Assert>; type _K_JSONRPCRequest = Assert>; type _K_JSONRPCNotification = Assert>; -type _K_EmptyResult = Assert>; +type _K_EmptyResult = Assert>; type _K_Notification = Assert>; type _K_ResourceTemplateReference = Assert>; -// @ts-expect-error Genuine mismatch: SDK PromptReference is missing 'title' from spec -type _K_PromptReference = Assert>; +type _K_PromptReference = Assert>; type _K_ToolAnnotations = Assert>; type _K_Tool = Assert>; -type _K_ListToolsResult = Assert>; -type _K_CallToolResult = Assert>; -type _K_ListResourcesResult = Assert>; -type _K_ListResourceTemplatesResult = Assert< - AssertExactKeysWithResultType ->; -type _K_ReadResourceResult = Assert>; +type _K_ListToolsResult = Assert>; +type _K_CallToolResult = Assert>; +type _K_ListResourcesResult = Assert>; +type _K_ListResourceTemplatesResult = Assert>; +type _K_ReadResourceResult = Assert>; type _K_ResourceContents = Assert>; type _K_TextResourceContents = Assert>; type _K_BlobResourceContents = Assert>; type _K_Resource = Assert>; -// @ts-expect-error Genuine mismatch: SDK PromptArgument is missing 'title' from spec -type _K_PromptArgument = Assert>; +type _K_PromptArgument = Assert>; type _K_Prompt = Assert>; -type _K_ListPromptsResult = Assert>; -type _K_GetPromptResult = Assert>; +type _K_ListPromptsResult = Assert>; +type _K_GetPromptResult = Assert>; type _K_TextContent = Assert>; type _K_ImageContent = Assert>; type _K_AudioContent = Assert>; @@ -764,11 +767,9 @@ type _K_TitledMultiSelectEnumSchema = Assert>; type _K_JSONRPCErrorResponse = Assert>; type _K_JSONRPCResultResponse = Assert>; -type _K_InitializeResult = Assert>; -// @ts-expect-error SDK follows the 2026-07-28 schema's `extensions` capability key; not in released 2025-11-25 -type _K_ClientCapabilities = Assert>; -// @ts-expect-error SDK follows the 2026-07-28 schema's `extensions` capability key; not in released 2025-11-25 -type _K_ServerCapabilities = Assert>; +type _K_InitializeResult = Assert>; +type _K_ClientCapabilities = Assert>; +type _K_ServerCapabilities = Assert>; type _K_SamplingMessage = Assert>; type _K_Icon = Assert>; type _K_Icons = Assert>; @@ -783,11 +784,11 @@ type _K_TaskMetadata = Assert>; type _K_TaskAugmentedRequestParams = Assert>; type _K_Task = Assert>; -type _K_CreateTaskResult = Assert>; -type _K_GetTaskResult = Assert>; -type _K_GetTaskPayloadResult = Assert>; -type _K_ListTasksResult = Assert>; -type _K_CancelTaskResult = Assert>; +type _K_CreateTaskResult = Assert>; +type _K_GetTaskResult = Assert>; +type _K_GetTaskPayloadResult = Assert>; +type _K_ListTasksResult = Assert>; +type _K_CancelTaskResult = Assert>; type _K_TaskStatusNotificationParams = Assert< AssertExactKeys >; @@ -855,7 +856,7 @@ type _K_CancelTaskRequest = Assert>; +type _K_CreateMessageResult = Assert>; type _K_ResourceTemplate = Assert>; // Types excluded from the key-parity completeness guard: union types and primitive aliases diff --git a/packages/core/test/spec.types.2026-07-28.test.ts b/packages/core/test/spec.types.2026-07-28.test.ts index 064221963a..5bf80604c6 100644 --- a/packages/core/test/spec.types.2026-07-28.test.ts +++ b/packages/core/test/spec.types.2026-07-28.test.ts @@ -1,15 +1,20 @@ /** - * Compares the SDK's types against the upcoming 2026-07-28 schema (spec.types.2026-07-28.ts). - * The frozen-release comparison lives in spec.types.2025-11-25.test.ts. + * Per-revision parity: the 2026-era WIRE artifacts against the 2026-07-28 + * anchor (spec.types.2026-07-28.ts). The frozen-release comparison lives in + * spec.types.2025-11-25.test.ts. * - * The SDK does not implement the 2026-07-28 surface yet: every 2026-07-28 type whose shape the SDK - * does not (yet) match is listed in MISSING_SDK_TYPES_2026_07_28 below. Removing a name from - * that list forces a real mutual-assignability check to be added to sdkTypeChecks (the - * completeness tests below fail otherwise) — implementation work burns the list down. + * Q1 increment 2 retired the old 67-name burn-down list (whose "permanent + * stratum" could never burn under a single shared schema set): the SDK now + * models era-specific wire shapes in `wire/rev2026-07-28/`, and everything + * that module models is compared here EXACTLY — wire-true request views + * (envelope-required `_meta`), resultType-required result wrappers, the + * forked Tool/SamplingMessage payloads, response envelopes, and discover. * - * Unlike MISSING_SDK_TYPES in the 2025-11-25 comparison, names in this list may well - * exist in the SDK (e.g. RequestParams) — they are listed because the 2026-07-28 revision changed - * their shape, not necessarily because the SDK lacks them. + * What remains unmodeled lives in FEATURE_OWNED_PENDING_2026 below: every + * entry is OWNED by a named feature issue and is stale-checked — adding a + * check for a pending name forces the entry's removal, and the completeness + * tests fail on any spec type that is neither checked nor owned. There is no + * permanent stratum: when the owning features land, the list reaches zero. */ import fs from 'node:fs'; import path from 'node:path'; @@ -21,6 +26,8 @@ import { } from '../src/types/spec.types.2026-07-28.js'; import type * as SpecTypes from '../src/types/spec.types.2026-07-28.js'; import type * as SDKTypes from '../src/types/index.js'; +import type * as Wire2026 from '../src/wire/rev2026-07-28/schemas.js'; +import type * as z4 from 'zod/v4'; import { CLIENT_CAPABILITIES_META_KEY, CLIENT_INFO_META_KEY, @@ -37,6 +44,40 @@ type WithJSONRPC = T & { jsonrpc: '2.0' }; // Adds the `jsonrpc` and `id` properties to a type, to match the on-wire format of requests. type WithJSONRPCRequest = T & { jsonrpc: '2.0'; id: SDKTypes.RequestId }; +/* The 2026-era wire artifacts under comparison (inferred from the era module's + * Zod schemas — the same objects the codec parses with). */ +type WResult = z4.infer; +type WResultType = z4.infer; +type WPaginatedResult = z4.infer; +type WCacheableResult = z4.infer; +type WCallToolResult = z4.infer; +type WCompleteResult = z4.infer; +type WGetPromptResult = z4.infer; +type WListPromptsResult = z4.infer; +type WListResourceTemplatesResult = z4.infer; +type WListResourcesResult = z4.infer; +type WListToolsResult = z4.infer; +type WReadResourceResult = z4.infer; +type WDiscoverResult = z4.infer; +type WTool = z4.infer; +type WSamplingMessage = z4.infer; +type WJSONRPCResultResponse = z4.infer; +type WCompleteRequest = z4.infer; +type WListPromptsRequest = z4.infer; +type WListResourceTemplatesRequest = z4.infer; +type WListResourcesRequest = z4.infer; +type WListToolsRequest = z4.infer; +type WReadResourceRequest = z4.infer; +type WDiscoverRequest = z4.infer; +// Param/base shapes derived from the request views (no second source of truth): +type WRequestParams = NonNullable; +type WPaginatedRequestParams = WListToolsRequest['params']; +type WResourceRequestParams = WReadResourceRequest['params']; +type WCompleteRequestParams = WCompleteRequest['params']; +// PaginatedRequest in the anchor keeps `method: string` (it is the base, not +// a concrete method) — composed from the derived params shape. +type WPaginatedRequest = WithJSONRPCRequest<{ method: string; params: WPaginatedRequestParams }>; + const sdkTypeChecks = { JSONValue: (sdk: SDKTypes.JSONValue, spec: SpecTypes.JSONValue) => { sdk = spec; @@ -358,6 +399,13 @@ const sdkTypeChecks = { sdk = spec; spec = sdk; }, + ElicitationCompleteNotificationParams: ( + sdk: SDKTypes.ElicitationCompleteNotificationParams, + spec: SpecTypes.ElicitationCompleteNotificationParams + ) => { + sdk = spec; + spec = sdk; + }, ElicitationCompleteNotification: ( sdk: WithJSONRPC, spec: SpecTypes.ElicitationCompleteNotification @@ -371,124 +419,245 @@ const sdkTypeChecks = { } }; +/* 2026-era wire parity checks (Q1 increment 2) — appended to sdkTypeChecks. */ +const wireParityChecks = { + Result: (sdk: WResult, spec: SpecTypes.Result) => { + sdk = spec; + spec = sdk; + }, + ResultType: (sdk: WResultType, spec: SpecTypes.ResultType) => { + sdk = spec; + spec = sdk; + }, + EmptyResult: (sdk: WResult, spec: SpecTypes.EmptyResult) => { + sdk = spec; + spec = sdk; + }, + ClientResult: (sdk: WResult, spec: SpecTypes.ClientResult) => { + sdk = spec; + spec = sdk; + }, + PaginatedResult: (sdk: WPaginatedResult, spec: SpecTypes.PaginatedResult) => { + sdk = spec; + spec = sdk; + }, + CacheableResult: (sdk: WCacheableResult, spec: SpecTypes.CacheableResult) => { + sdk = spec; + spec = sdk; + }, + CallToolResult: (sdk: WCallToolResult, spec: SpecTypes.CallToolResult) => { + sdk = spec; + spec = sdk; + }, + CompleteResult: (sdk: WCompleteResult, spec: SpecTypes.CompleteResult) => { + sdk = spec; + spec = sdk; + }, + GetPromptResult: (sdk: WGetPromptResult, spec: SpecTypes.GetPromptResult) => { + sdk = spec; + spec = sdk; + }, + ListPromptsResult: (sdk: WListPromptsResult, spec: SpecTypes.ListPromptsResult) => { + sdk = spec; + spec = sdk; + }, + ListResourceTemplatesResult: (sdk: WListResourceTemplatesResult, spec: SpecTypes.ListResourceTemplatesResult) => { + sdk = spec; + spec = sdk; + }, + ListResourcesResult: (sdk: WListResourcesResult, spec: SpecTypes.ListResourcesResult) => { + sdk = spec; + spec = sdk; + }, + ListToolsResult: (sdk: WListToolsResult, spec: SpecTypes.ListToolsResult) => { + sdk = spec; + spec = sdk; + }, + ReadResourceResult: (sdk: WReadResourceResult, spec: SpecTypes.ReadResourceResult) => { + sdk = spec; + spec = sdk; + }, + DiscoverResult: (sdk: WDiscoverResult, spec: SpecTypes.DiscoverResult) => { + sdk = spec; + spec = sdk; + }, + DiscoverRequest: (sdk: WithJSONRPCRequest, spec: SpecTypes.DiscoverRequest) => { + sdk = spec; + spec = sdk; + }, + Tool: (sdk: WTool, spec: SpecTypes.Tool) => { + sdk = spec; + spec = sdk; + }, + SamplingMessage: (sdk: WSamplingMessage, spec: SpecTypes.SamplingMessage) => { + sdk = spec; + spec = sdk; + }, + SamplingMessageContentBlock: ( + sdk: z4.infer, + spec: SpecTypes.SamplingMessageContentBlock + ) => { + sdk = spec; + spec = sdk; + }, + ToolResultContent: (sdk: z4.infer, spec: SpecTypes.ToolResultContent) => { + sdk = spec; + spec = sdk; + }, + Notification: (sdk: SDKTypes.Notification, spec: SpecTypes.Notification) => { + sdk = spec; + spec = sdk; + }, + JSONRPCResultResponse: (sdk: WJSONRPCResultResponse, spec: SpecTypes.JSONRPCResultResponse) => { + sdk = spec; + spec = sdk; + }, + JSONRPCResponse: (sdk: WJSONRPCResultResponse | SDKTypes.JSONRPCErrorResponse, spec: SpecTypes.JSONRPCResponse) => { + sdk = spec; + spec = sdk; + }, + JSONRPCMessage: ( + sdk: SDKTypes.JSONRPCRequest | WithJSONRPC | WJSONRPCResultResponse | SDKTypes.JSONRPCErrorResponse, + spec: SpecTypes.JSONRPCMessage + ) => { + sdk = spec; + spec = sdk; + }, + CompleteResultResponse: (sdk: z4.infer, spec: SpecTypes.CompleteResultResponse) => { + sdk = spec; + spec = sdk; + }, + ListPromptsResultResponse: ( + sdk: z4.infer, + spec: SpecTypes.ListPromptsResultResponse + ) => { + sdk = spec; + spec = sdk; + }, + ListResourceTemplatesResultResponse: ( + sdk: z4.infer, + spec: SpecTypes.ListResourceTemplatesResultResponse + ) => { + sdk = spec; + spec = sdk; + }, + ListResourcesResultResponse: ( + sdk: z4.infer, + spec: SpecTypes.ListResourcesResultResponse + ) => { + sdk = spec; + spec = sdk; + }, + ListToolsResultResponse: (sdk: z4.infer, spec: SpecTypes.ListToolsResultResponse) => { + sdk = spec; + spec = sdk; + }, + DiscoverResultResponse: (sdk: z4.infer, spec: SpecTypes.DiscoverResultResponse) => { + sdk = spec; + spec = sdk; + }, + RequestParams: (sdk: WRequestParams, spec: SpecTypes.RequestParams) => { + sdk = spec; + spec = sdk; + }, + PaginatedRequestParams: (sdk: WPaginatedRequestParams, spec: SpecTypes.PaginatedRequestParams) => { + sdk = spec; + spec = sdk; + }, + ResourceRequestParams: (sdk: WResourceRequestParams, spec: SpecTypes.ResourceRequestParams) => { + sdk = spec; + spec = sdk; + }, + CompleteRequestParams: (sdk: WCompleteRequestParams, spec: SpecTypes.CompleteRequestParams) => { + sdk = spec; + spec = sdk; + }, + PaginatedRequest: (sdk: WPaginatedRequest, spec: SpecTypes.PaginatedRequest) => { + sdk = spec; + spec = sdk; + }, + CompleteRequest: (sdk: WithJSONRPCRequest, spec: SpecTypes.CompleteRequest) => { + sdk = spec; + spec = sdk; + }, + ListPromptsRequest: (sdk: WithJSONRPCRequest, spec: SpecTypes.ListPromptsRequest) => { + sdk = spec; + spec = sdk; + }, + ListResourceTemplatesRequest: ( + sdk: WithJSONRPCRequest, + spec: SpecTypes.ListResourceTemplatesRequest + ) => { + sdk = spec; + spec = sdk; + }, + ListResourcesRequest: (sdk: WithJSONRPCRequest, spec: SpecTypes.ListResourcesRequest) => { + sdk = spec; + spec = sdk; + }, + ListToolsRequest: (sdk: WithJSONRPCRequest, spec: SpecTypes.ListToolsRequest) => { + sdk = spec; + spec = sdk; + } +}; + +const allTypeChecks = { ...sdkTypeChecks, ...wireParityChecks }; + // Generated from the 2026-07-28 schema by `pnpm run fetch:spec-types 2026-07-28 `. const SPEC_TYPES_FILE = path.resolve(__dirname, '../src/types/spec.types.2026-07-28.ts'); /** - * 2026-07-28 spec types the SDK does not match yet. Spec-implementation work for the - * 2026-07-28 release removes entries from this list as the SDK adopts each shape. + * Spec types the 2026-era wire module does not model yet — every entry is + * OWNED by a named feature issue (no permanent stratum; the list reaches + * zero as the owners land). Adding a parity check for one of these names + * forces the entry's removal (stale-check below). */ -const MISSING_SDK_TYPES_2026_07_28 = [ +const FEATURE_OWNED_PENDING_2026: Record = { // Inlined in the SDK (same as the 2025-11-25 comparison): - 'Error', // The inner error object of a JSONRPCError - - // SEP-2575 per-request envelope: 2026-07-28 requests REQUIRE a `_meta` envelope - // (`io.modelcontextprotocol/protocolVersion`, clientInfo, clientCapabilities). The - // envelope itself is modeled by RequestMetaEnvelope (see sdkTypeChecks above); the - // request shapes below stay here because the SDK wire schemas deliberately keep - // `_meta` lenient — the same schemas parse pre-2026 requests (no envelope) and 2026 - // requests, with envelope requiredness enforced per request at dispatch. They burn - // only if the SDK ever models era-specific request types. - 'RequestParams', - 'PaginatedRequestParams', - 'ResourceRequestParams', - 'CallToolRequestParams', - 'CompleteRequestParams', - 'GetPromptRequestParams', - 'ReadResourceRequestParams', - 'CreateMessageRequestParams', - 'PaginatedRequest', - 'CallToolRequest', - 'CompleteRequest', - 'GetPromptRequest', - 'ListPromptsRequest', - 'ListResourceTemplatesRequest', - 'ListResourcesRequest', - 'ListRootsRequest', - 'ListToolsRequest', - 'ReadResourceRequest', - 'CreateMessageRequest', - 'ClientRequest', - - // SEP-2322 (MRTR) → PR for MRTR: 2026-07-28 results carry a required `resultType` - // discriminator. The SDK base result schema carries `resultType` as an optional - // passthrough only (absent means "complete"); per-result modeling lands with MRTR. - 'Result', - 'EmptyResult', - 'PaginatedResult', - 'CallToolResult', - 'CompleteResult', - 'ElicitResult', - 'GetPromptResult', - 'ListPromptsResult', - 'ListResourceTemplatesResult', - 'ListResourcesResult', - 'ListRootsResult', - 'ListToolsResult', - 'ReadResourceResult', - 'CreateMessageResult', - 'ClientResult', - 'ServerResult', - 'ResultType', - - // SEP-2549 cacheable results: `ttlMs`/`cacheScope` caching hints on the list/read - // result shapes → PR for SEP-2549: - 'CacheableResult', + Error: 'the inner error object of a JSONRPCError is inlined in the SDK', - // Response envelopes embedding the changed Result shape → PR for MRTR: - 'JSONRPCResultResponse', - 'JSONRPCResponse', - 'JSONRPCMessage', - 'CallToolResultResponse', - 'CompleteResultResponse', - 'GetPromptResultResponse', - 'ListPromptsResultResponse', - 'ListResourceTemplatesResultResponse', - 'ListResourcesResultResponse', - 'ListToolsResultResponse', - 'ReadResourceResultResponse', + // M4.1 MRTR (#13): the in-band input-request surface and the demoted + // sampling/elicitation/roots shapes (wire requests in 2025, in-band + // InputRequest payloads in 2026 — the SDK models them when the + // multi-round-trip driver lands): + InputRequest: 'M4.1 MRTR (#13)', + InputRequests: 'M4.1 MRTR (#13)', + InputRequiredResult: 'M4.1 MRTR (#13)', + InputResponse: 'M4.1 MRTR (#13)', + InputResponseRequestParams: 'M4.1 MRTR (#13)', + InputResponses: 'M4.1 MRTR (#13)', + CreateMessageRequest: 'M4.1 MRTR (#13) — demoted to an in-band payload in 2026', + CreateMessageRequestParams: 'M4.1 MRTR (#13) — demoted to an in-band payload in 2026', + CreateMessageResult: 'M4.1 MRTR (#13) — in-band response shape', + ElicitResult: 'M4.1 MRTR (#13) — in-band response shape', + ListRootsRequest: 'M4.1 MRTR (#13) — demoted to an in-band payload in 2026', + ListRootsResult: 'M4.1 MRTR (#13) — in-band response shape', + ServerResult: 'M4.1 MRTR (#13) — the union gains InputRequiredResult', + CallToolRequestParams: 'M4.1 MRTR (#13) — params extend InputResponseRequestParams (the retry channel)', + CallToolRequest: 'M4.1 MRTR (#13) — params extend InputResponseRequestParams (the retry channel)', + GetPromptRequestParams: 'M4.1 MRTR (#13) — params extend InputResponseRequestParams (the retry channel)', + GetPromptRequest: 'M4.1 MRTR (#13) — params extend InputResponseRequestParams (the retry channel)', + ReadResourceRequestParams: 'M4.1 MRTR (#13) — params extend InputResponseRequestParams (the retry channel)', + ReadResourceRequest: 'M4.1 MRTR (#13) — params extend InputResponseRequestParams (the retry channel)', + CallToolResultResponse: 'M4.1 MRTR (#13) — the result union gains InputRequiredResult', + GetPromptResultResponse: 'M4.1 MRTR (#13) — the result union gains InputRequiredResult', + ReadResourceResultResponse: 'M4.1 MRTR (#13) — the result union gains InputRequiredResult', - // SEP-2575 sessionless discovery: the SDK ships the wire shapes - // (DiscoverRequestSchema / DiscoverResultSchema), but the 2026-07-28 shapes embed the - // required `_meta` envelope (request) and required `resultType` (result → MRTR PR), - // so they do not match yet; DiscoverResultResponse is a response wrapper (→ MRTR PR): - 'DiscoverRequest', - 'DiscoverResult', - 'DiscoverResultResponse', + // M6.1 subscriptions/listen (#14): + SubscriptionFilter: 'M6.1 subscriptions/listen (#14)', + SubscriptionsAcknowledgedNotification: 'M6.1 subscriptions/listen (#14)', + SubscriptionsAcknowledgedNotificationParams: 'M6.1 subscriptions/listen (#14)', + SubscriptionsListenRequest: 'M6.1 subscriptions/listen (#14)', + SubscriptionsListenRequestParams: 'M6.1 subscriptions/listen (#14)', + ClientRequest: 'M6.1 subscriptions/listen (#14) — the union gains SubscriptionsListenRequest', + ServerNotification: 'M6.1 subscriptions/listen (#14) — the union gains the acknowledged notification', - // SEP-2567 input requests/responses (new surface) → PR for MRTR: - 'InputRequest', - 'InputRequests', - 'InputRequiredResult', - 'InputResponse', - 'InputResponseRequestParams', - 'InputResponses', - - // 2026-07-28 subscriptions surface (new) → PR for subscriptions/listen: - 'SubscriptionFilter', - 'SubscriptionsAcknowledgedNotification', - 'SubscriptionsAcknowledgedNotificationParams', - 'SubscriptionsListenRequest', - 'SubscriptionsListenRequestParams', - - // New typed protocol errors: the SDK ships -32003/-32004 as ProtocolErrorCode - // entries plus the UnsupportedProtocolVersionError class (errors.ts); the spec's - // per-code error *response envelope* interfaces are not modeled as wire types: - 'MissingRequiredClientCapabilityError', - 'UnsupportedProtocolVersionError', + // M1.2 validation ladder (#8): the per-code error response envelopes: + MissingRequiredClientCapabilityError: 'M1.2 validation ladder (#8)', + UnsupportedProtocolVersionError: 'M1.2 validation ladder (#8)' +}; - // Other shapes changed in the 2026-07-28 schema: sampling content changes (SamplingMessage, - // SamplingMessageContentBlock, ToolResultContent) → backchannel PR; open tool - // input/output schema typing (Tool); loosened Notification.params (Notification); - // server notification union, which gains the subscriptions ack (ServerNotification → - // PR for subscriptions/listen): - 'SamplingMessage', - 'SamplingMessageContentBlock', - 'ToolResultContent', - 'Tool', - 'Notification', - 'ServerNotification' -]; +const MISSING_SDK_TYPES_2026_07_28 = Object.keys(FEATURE_OWNED_PENDING_2026); function extractExportedTypes(source: string): string[] { const matches = [...source.matchAll(/export\s+(?:interface|class|type)\s+(\w+)\b/g)]; @@ -518,7 +687,7 @@ describe('Spec Types (2026-07-28)', () => { expect(specTypes).toContain('DiscoverRequest'); expect(specTypes).toContain('InputRequiredResult'); expect(specTypes).toContain('SubscriptionsListenRequest'); - expect(specTypes).toHaveLength(150); + expect(specTypes).toHaveLength(151); }); it('should only allowlist types that exist in the 2026-07-28 schema', () => { @@ -531,7 +700,7 @@ describe('Spec Types (2026-07-28)', () => { const missingTests = []; for (const typeName of typesToCheck) { - if (!sdkTypeChecks[typeName as keyof typeof sdkTypeChecks]) { + if (!allTypeChecks[typeName as keyof typeof allTypeChecks]) { missingTests.push(typeName); } } @@ -539,12 +708,15 @@ describe('Spec Types (2026-07-28)', () => { expect(missingTests).toHaveLength(0); }); - describe('Missing SDK Types', () => { - it.each(MISSING_SDK_TYPES_2026_07_28)( - '%s should not be present in MISSING_SDK_TYPES_2026_07_28 if it has a compatibility test', - type => { - expect(sdkTypeChecks[type as keyof typeof sdkTypeChecks]).toBeUndefined(); + describe('Feature-owned pending entries', () => { + it.each(MISSING_SDK_TYPES_2026_07_28)('%s must not be pending once it has a parity check (stale-check)', type => { + expect(allTypeChecks[type as keyof typeof allTypeChecks]).toBeUndefined(); + }); + + it('every pending entry names its owner', () => { + for (const [name, owner] of Object.entries(FEATURE_OWNED_PENDING_2026)) { + expect(owner.length, name).toBeGreaterThan(0); } - ); + }); }); }); diff --git a/packages/core/test/types.test.ts b/packages/core/test/types.test.ts index a92615bceb..70b0b02a82 100644 --- a/packages/core/test/types.test.ts +++ b/packages/core/test/types.test.ts @@ -18,7 +18,6 @@ import { LOG_LEVEL_META_KEY, PromptMessageSchema, PROTOCOL_VERSION_META_KEY, - RequestMetaEnvelopeSchema, ResourceLinkSchema, ResultSchema, SamplingMessageSchema, @@ -28,6 +27,12 @@ import { ToolSchema, ToolUseContentSchema } from '../src/types/index.js'; +// Wire-era modules (Q1 increment 2): the per-request envelope lives in the +// 2026-era schemas; the era-faithful 2025 role unions (incl. tasks) live in +// the 2025-era schemas. +import { getRequestSchema } from '../src/wire/rev2025-11-25/registry.js'; +import { ClientRequestSchema as Wire2025ClientRequestSchema } from '../src/wire/rev2025-11-25/schemas.js'; +import { RequestMetaEnvelopeSchema } from '../src/wire/rev2026-07-28/schemas.js'; describe('Types', () => { test('should have correct latest protocol version', () => { @@ -291,10 +296,13 @@ describe('Types', () => { } }); - test('should validate empty content array with default', () => { - const toolResult = {}; - - const result = CallToolResultSchema.safeParse(toolResult); + test('requires content: the empty-object result no longer parses (deliberate flip)', () => { + // BEHAVIOR MIGRATION (Q1 increment 2, ledgered): content.default([]) + // was removed from the wire schema (the T6 silent-empty-success + // masking root). Content is spec-required in every revision. + // Changeset: codec-split-wire-break. + expect(CallToolResultSchema.safeParse({}).success).toBe(false); + const result = CallToolResultSchema.safeParse({ content: [] }); expect(result.success).toBe(true); if (result.success) { expect(result.data.content).toEqual([]); @@ -567,6 +575,9 @@ describe('Types', () => { const toolResult = { type: 'tool_result', toolUseId: 'call_123', + // content is spec-required (the wire default([]) was removed — + // Q1 increment 2, ledgered; changeset: codec-split-wire-break). + content: [], structuredContent: { temperature: 72, condition: 'sunny' } }; @@ -583,6 +594,7 @@ describe('Types', () => { const toolResult = { type: 'tool_result', toolUseId: 'call_456', + content: [], structuredContent: { error: 'API_ERROR', message: 'Service unavailable' }, isError: true }; @@ -1025,9 +1037,15 @@ describe('Types', () => { }); describe('2025-11-25 task wire interop (task feature removed; wire types remain)', () => { - test('tasks/get parses through the client request union', () => { - const result = ClientRequestSchema.safeParse({ method: 'tasks/get', params: { taskId: 'task-123' } }); + test('tasks/get parses through the 2025-era wire request union and registry', () => { + // The task wire surface moved into the 2025-era codec module (Q1 + // increment 2): interop with task-capable 2025 peers is served by the + // era registry, and the NEUTRAL ClientRequestSchema no longer carries + // task vocabulary (deletions are physical on the 2026 era). + const result = Wire2025ClientRequestSchema.safeParse({ method: 'tasks/get', params: { taskId: 'task-123' } }); expect(result.success).toBe(true); + expect(getRequestSchema('tasks/get')).toBeDefined(); + expect(ClientRequestSchema.options.some(option => (option.shape.method.value as string) === 'tasks/get')).toBe(false); }); test('task-augmented tools/call params parse and retain the task field', () => { @@ -1148,26 +1166,25 @@ describe('2026-07-28 wire shapes', () => { }); }); - describe('Result resultType passthrough', () => { - test('accepts results with and without resultType (absent means "complete")', () => { + describe('Result resultType (cut from the neutral schemas — Q1 increment 2, ledgered)', () => { + test('the base ResultSchema no longer declares resultType; the key is loose passthrough only', () => { + // BEHAVIOR MIGRATION: the optional resultType member — the + // masking surface that let 2026 vocabulary through every + // legacy-leg parse — is gone. The wire member lives only in the + // 2026-era codec module. A foreign resultType still transits the + // loose base parse as an UNDECLARED sibling (it can no longer + // type-check, and the protocol path strips/consumes it per era). const withIt = ResultSchema.safeParse({ resultType: 'complete' }); expect(withIt.success).toBe(true); - if (withIt.success) { - expect(withIt.data.resultType).toBe('complete'); - } - const withoutIt = ResultSchema.safeParse({}); - expect(withoutIt.success).toBe(true); - if (withoutIt.success) { - expect(withoutIt.data.resultType).toBeUndefined(); - } - }); - - test('rejects a non-string resultType', () => { - expect(ResultSchema.safeParse({ resultType: 42 }).success).toBe(false); + // Non-string values are no longer schema-rejected here (the + // member is undeclared): era handling owns the raw value. + expect(ResultSchema.safeParse({ resultType: 42 }).success).toBe(true); + expect(Object.keys(ResultSchema.shape)).toEqual(['_meta']); }); - test('EmptyResult accepts resultType but still rejects unknown keys', () => { - expect(EmptyResultSchema.safeParse({ resultType: 'complete' }).success).toBe(true); + test('EmptyResult rejects resultType like any unknown key (deliberate flip)', () => { + // Changeset: codec-split-wire-break. + expect(EmptyResultSchema.safeParse({ resultType: 'complete' }).success).toBe(false); expect(EmptyResultSchema.safeParse({ unexpected: true }).success).toBe(false); }); }); diff --git a/packages/core/test/types/crossBundleErrorRecognition.test.ts b/packages/core/test/types/crossBundleErrorRecognition.test.ts new file mode 100644 index 0000000000..35f69acbae --- /dev/null +++ b/packages/core/test/types/crossBundleErrorRecognition.test.ts @@ -0,0 +1,131 @@ +/** + * Cross-bundle typed-error recognition guard. + * + * The core package is bundled separately into the client and server dists, so + * a typed error class constructed inside one bundle is NOT `instanceof` the + * "same" class imported from another bundle. The recognition contract is + * therefore: typed protocol errors are materialized from the wire shape — + * numeric `code` plus structurally parsed `error.data` — and consumers (and + * the SDK itself) must never rely on `instanceof` across the package boundary. + * + * These tests pin that contract from both directions: + * - recognition succeeds for plain wire values and for foreign-prototype + * instances (simulating an error object created by another bundled copy of + * core), and + * - recognition is purely structural — malformed `data` falls back to the + * generic class rather than guessing or throwing. + */ +import { describe, expect, test } from 'vitest'; + +import type { BaseContext } from '../../src/shared/protocol.js'; +import { Protocol } from '../../src/shared/protocol.js'; +import { InMemoryTransport } from '../../src/util/inMemory.js'; +import type { JSONRPCRequest } from '../../src/types/index.js'; +import { ProtocolError, ProtocolErrorCode, UnsupportedProtocolVersionError, UrlElicitationRequiredError } from '../../src/types/index.js'; + +class TestProtocol extends Protocol { + protected assertCapabilityForMethod(): void {} + protected assertNotificationCapability(): void {} + protected assertRequestHandlerCapability(): void {} + protected buildContext(ctx: BaseContext): BaseContext { + return ctx; + } +} + +/** + * A structural twin of `UnsupportedProtocolVersionError` with its own + * prototype chain — what an error created by a second bundled copy of core + * looks like to this copy: same name, same fields, different identity. + */ +class ForeignUnsupportedProtocolVersionError extends Error { + readonly code = -32_004; + readonly data = { supported: ['2025-11-25'], requested: '2099-01-01' }; + constructor() { + super('Unsupported protocol version: 2099-01-01'); + this.name = 'UnsupportedProtocolVersionError'; + } +} + +describe('cross-bundle typed-error recognition (data parse, never instanceof)', () => { + test('a -32004 error received over the wire materializes the typed class from code + data', async () => { + // Full dispatch round trip: the peer answers with a plain JSON error + // body — exactly what crosses a transport (and a bundle) boundary. + const [clientTx, serverTx] = InMemoryTransport.createLinkedPair(); + serverTx.onmessage = message => { + const request = message as JSONRPCRequest; + void serverTx.send({ + jsonrpc: '2.0', + id: request.id, + error: { + code: -32_004, + message: 'Unsupported protocol version', + data: { supported: ['2025-11-25', '2025-06-18'], requested: '2099-01-01' } + } + }); + }; + await serverTx.start(); + + const protocol = new TestProtocol(); + await protocol.connect(clientTx); + + const rejection = await protocol.request({ method: 'ping' }).catch((error: unknown) => error); + + // The receiving side gets the typed class, materialized purely from + // the wire shape (numeric code + structurally valid data). + expect(rejection).toBeInstanceOf(UnsupportedProtocolVersionError); + const typed = rejection as UnsupportedProtocolVersionError; + expect(typed.code).toBe(ProtocolErrorCode.UnsupportedProtocolVersion); + expect(typed.supported).toEqual(['2025-11-25', '2025-06-18']); + expect(typed.requested).toBe('2099-01-01'); + + await protocol.close(); + }); + + test('recognition works for a foreign-prototype instance via its code/data, not its identity', () => { + const foreign = new ForeignUnsupportedProtocolVersionError(); + + // The foreign instance is NOT instanceof this bundle's classes — the + // exact situation `instanceof` checks silently get wrong. + expect(foreign instanceof UnsupportedProtocolVersionError).toBe(false); + expect(foreign instanceof ProtocolError).toBe(false); + + // Recognition through the wire shape still succeeds. + const recognized = ProtocolError.fromError(foreign.code, foreign.message, foreign.data); + expect(recognized).toBeInstanceOf(UnsupportedProtocolVersionError); + expect((recognized as UnsupportedProtocolVersionError).supported).toEqual(['2025-11-25']); + expect((recognized as UnsupportedProtocolVersionError).requested).toBe('2099-01-01'); + }); + + test('recognition survives JSON serialization (no prototype information required)', () => { + // Serialize a locally constructed typed error down to its wire shape + // and re-recognize it — the round trip a bundled boundary forces. + const original = new UrlElicitationRequiredError([ + { mode: 'url', message: 'visit', url: 'https://example.com/elicit', elicitationId: 'e1' } + ]); + const wireShape = JSON.parse(JSON.stringify({ code: original.code, message: original.message, data: original.data })) as { + code: number; + message: string; + data: unknown; + }; + + const recognized = ProtocolError.fromError(wireShape.code, wireShape.message, wireShape.data); + expect(recognized).toBeInstanceOf(UrlElicitationRequiredError); + expect((recognized as UrlElicitationRequiredError).elicitations).toHaveLength(1); + expect((recognized as UrlElicitationRequiredError).elicitations[0]?.url).toBe('https://example.com/elicit'); + }); + + test('structurally invalid data falls back to the generic class — no guess, no throw', () => { + // -32004 with data that does not parse as UnsupportedProtocolVersionErrorData. + for (const data of [undefined, null, 'nope', { supported: 'not-an-array', requested: '2099-01-01' }, { wrong: 'shape' }]) { + const recognized = ProtocolError.fromError(-32_004, 'unsupported', data); + expect(recognized).toBeInstanceOf(ProtocolError); + expect(recognized).not.toBeInstanceOf(UnsupportedProtocolVersionError); + expect(recognized.code).toBe(-32_004); + } + + // -32042 with data missing the elicitations array. + const urlFallback = ProtocolError.fromError(-32_042, 'elicitation required', { other: true }); + expect(urlFallback).toBeInstanceOf(ProtocolError); + expect(urlFallback).not.toBeInstanceOf(UrlElicitationRequiredError); + }); +}); diff --git a/packages/core/test/types/errorSurfacePins.test.ts b/packages/core/test/types/errorSurfacePins.test.ts new file mode 100644 index 0000000000..bb5fb64325 --- /dev/null +++ b/packages/core/test/types/errorSurfacePins.test.ts @@ -0,0 +1,162 @@ +/** + * Behavior-surface pins: error codes, error classes, and version constants. + * + * Consumers match SDK errors by literal numeric code, `error.name`, and message + * text — not only by enum member or `instanceof` (which breaks across bundled + * package boundaries). These tests pin the literal values so that a renumber, + * rename, or membership change turns CI red instead of landing silently. A + * failing pin here means the change is deliberate: update the pin in the same + * change, together with a changeset and a migration-doc entry. + * + * See docs/behavior-surface-pins.md for the maintenance protocol. + */ +import { describe, expect, test } from 'vitest'; + +import { SdkError, SdkErrorCode, SdkHttpError } from '../../src/errors/sdkErrors.js'; +import { + DEFAULT_NEGOTIATED_PROTOCOL_VERSION, + INTERNAL_ERROR, + INVALID_PARAMS, + INVALID_REQUEST, + JSONRPC_VERSION, + LATEST_PROTOCOL_VERSION, + METHOD_NOT_FOUND, + PARSE_ERROR, + ProtocolError, + ProtocolErrorCode, + SUPPORTED_PROTOCOL_VERSIONS, + UnsupportedProtocolVersionError, + UrlElicitationRequiredError +} from '../../src/types/index.js'; +import { STDIO_DEFAULT_MAX_BUFFER_SIZE } from '../../src/shared/stdio.js'; + +describe('ProtocolErrorCode', () => { + test('numeric values are frozen wire ABI', () => { + // Consumers map wire error codes by numeric value (value-to-label tables, + // duck-typed {code} checks across package boundaries), so the literal values + // are public ABI. Exact-equality on the whole table also locks membership in + // both directions: adding or removing a member is a deliberate act. + const members = Object.fromEntries(Object.entries(ProtocolErrorCode).filter(([key]) => Number.isNaN(Number(key)))); + expect(members).toEqual({ + ParseError: -32700, + InvalidRequest: -32600, + MethodNotFound: -32601, + InvalidParams: -32602, + InternalError: -32603, + ResourceNotFound: -32002, + MissingRequiredClientCapability: -32003, + UnsupportedProtocolVersion: -32004, + UrlElicitationRequired: -32042 + }); + }); + + test('bare JSON-RPC constant values are frozen', () => { + expect(PARSE_ERROR).toBe(-32700); + expect(INVALID_REQUEST).toBe(-32600); + expect(METHOD_NOT_FOUND).toBe(-32601); + expect(INVALID_PARAMS).toBe(-32602); + expect(INTERNAL_ERROR).toBe(-32603); + expect(JSONRPC_VERSION).toBe('2.0'); + }); +}); + +describe('SdkErrorCode', () => { + test('string values are frozen ABI', () => { + // SDK errors are local (never serialized to the wire) but consumers still + // branch on the literal string codes, so the values and the membership of + // the enum are pinned in both directions. + expect({ ...SdkErrorCode }).toEqual({ + NotConnected: 'NOT_CONNECTED', + AlreadyConnected: 'ALREADY_CONNECTED', + NotInitialized: 'NOT_INITIALIZED', + CapabilityNotSupported: 'CAPABILITY_NOT_SUPPORTED', + RequestTimeout: 'REQUEST_TIMEOUT', + ConnectionClosed: 'CONNECTION_CLOSED', + SendFailed: 'SEND_FAILED', + InvalidResult: 'INVALID_RESULT', + UnsupportedResultType: 'UNSUPPORTED_RESULT_TYPE', + MethodNotSupportedByProtocolVersion: 'METHOD_NOT_SUPPORTED_BY_PROTOCOL_VERSION', + ClientHttpNotImplemented: 'CLIENT_HTTP_NOT_IMPLEMENTED', + ClientHttpAuthentication: 'CLIENT_HTTP_AUTHENTICATION', + ClientHttpForbidden: 'CLIENT_HTTP_FORBIDDEN', + ClientHttpUnexpectedContent: 'CLIENT_HTTP_UNEXPECTED_CONTENT', + ClientHttpFailedToOpenStream: 'CLIENT_HTTP_FAILED_TO_OPEN_STREAM', + ClientHttpFailedToTerminateSession: 'CLIENT_HTTP_FAILED_TO_TERMINATE_SESSION' + }); + }); +}); + +describe('ProtocolError', () => { + test('sets error.name, carries code/data, and leaves the message verbatim', () => { + // Consumers classify errors via err.name (instanceof breaks when core is + // bundled into both the client and server dists), and read .code/.data as + // a duck shape. The constructor must not decorate the message. + const error = new ProtocolError(ProtocolErrorCode.InvalidParams, 'oops', { extra: 1 }); + expect(error.name).toBe('ProtocolError'); + expect(error.code).toBe(-32602); + expect(error.data).toEqual({ extra: 1 }); + expect(error.message).toBe('oops'); + expect(error).toBeInstanceOf(Error); + }); + + test('fromError materializes typed errors from code + parsed data, not instanceof', () => { + // Cross-bundle recognition contract: typed error classes are reconstructed + // from the wire shape (numeric code + structurally valid data). The inputs + // here are plain values, exactly what arrives across a package boundary. + const urlError = ProtocolError.fromError(-32042, 'elicitation required', { + elicitations: [{ mode: 'url', message: 'visit', url: 'https://example.com', elicitationId: 'e1' }] + }); + expect(urlError).toBeInstanceOf(UrlElicitationRequiredError); + expect((urlError as UrlElicitationRequiredError).elicitations).toHaveLength(1); + + const versionError = ProtocolError.fromError(-32004, 'unsupported', { supported: ['2025-11-25'], requested: '1999-01-01' }); + expect(versionError).toBeInstanceOf(UnsupportedProtocolVersionError); + expect((versionError as UnsupportedProtocolVersionError).supported).toEqual(['2025-11-25']); + expect((versionError as UnsupportedProtocolVersionError).requested).toBe('1999-01-01'); + + // Malformed/missing data falls back to the generic class instead of throwing. + const generic = ProtocolError.fromError(-32004, 'unsupported', { wrong: 'shape' }); + expect(generic).toBeInstanceOf(ProtocolError); + expect(generic).not.toBeInstanceOf(UnsupportedProtocolVersionError); + }); +}); + +describe('SdkError', () => { + test('sets error.name and carries the string code', () => { + const error = new SdkError(SdkErrorCode.RequestTimeout, 'Request timed out', { timeout: 60000 }); + expect(error.name).toBe('SdkError'); + expect(error.code).toBe('REQUEST_TIMEOUT'); + expect(error.data).toEqual({ timeout: 60000 }); + expect(error.message).toBe('Request timed out'); + }); + + test('SdkHttpError carries the HTTP status in data', () => { + const error = new SdkHttpError(SdkErrorCode.ClientHttpFailedToOpenStream, 'Failed to open SSE stream: Not Found', { + status: 404, + statusText: 'Not Found' + }); + expect(error.name).toBe('SdkHttpError'); + expect(error.code).toBe('CLIENT_HTTP_FAILED_TO_OPEN_STREAM'); + expect(error.data).toMatchObject({ status: 404 }); + }); +}); + +describe('protocol version constants', () => { + test('values and membership are frozen', () => { + // The supported list is pinned by exact value (not just membership) so a + // naive LATEST bump that silently drops a previous version goes red here. + expect(LATEST_PROTOCOL_VERSION).toBe('2025-11-25'); + expect(DEFAULT_NEGOTIATED_PROTOCOL_VERSION).toBe('2025-03-26'); + expect(SUPPORTED_PROTOCOL_VERSIONS).toEqual(['2025-11-25', '2025-06-18', '2025-03-26', '2024-11-05', '2024-10-07']); + expect(SUPPORTED_PROTOCOL_VERSIONS).toContain(LATEST_PROTOCOL_VERSION); + expect(SUPPORTED_PROTOCOL_VERSIONS).toContain(DEFAULT_NEGOTIATED_PROTOCOL_VERSION); + }); +}); + +describe('stdio framing constants', () => { + test('the default read-buffer cap is 10 MiB', () => { + // Public export consumed by custom transport authors; raising or lowering + // the cap changes which deployed payloads parse, so the value is pinned. + expect(STDIO_DEFAULT_MAX_BUFFER_SIZE).toBe(10 * 1024 * 1024); + }); +}); diff --git a/packages/core/test/types/registryPins.test.ts b/packages/core/test/types/registryPins.test.ts new file mode 100644 index 0000000000..73222b8eb5 --- /dev/null +++ b/packages/core/test/types/registryPins.test.ts @@ -0,0 +1,198 @@ +/** + * Registry byte-identity pre-pins for the wire-layer re-homing (Q1 increment 2). + * + * These tests pin the EXACT contents of the runtime method registries — + * method sets and per-method schema identity (by object reference) — so that + * relocating the registries behind the per-era codec interface is provably + * mechanical: the same schema objects must serve the same methods before and + * after the move. They are committed BEFORE the relocation lands (suite, then + * move — Q10-L2 ordering). + * + * The 2025-era registry is behavior-frozen: the request/notification maps + * carry the full deliberate 2025-11-25 wire vocabulary, including the task + * family (#2248 wire-interop restore). The RESULT map is the runtime/typed + * ALIGNED map (PR #2293 review fix): plain per-method schemas keyed by + * `RequestMethod` — no task-result union members and no `tasks/*` entries + * (task-method interop goes through the explicit-schema overload; see + * `test/shared/typedMapAlignment.test.ts` for the behavioral pins). Do not + * edit these pins to make a refactor pass; a pin change is a wire-behavior + * decision and needs a changeset + migration entry (Q10-L2). + */ +import { describe, expect, it } from 'vitest'; + +import { + CallToolRequestSchema, + CallToolResultSchema, + CancelledNotificationSchema, + CompleteRequestSchema, + CompleteResultSchema, + CreateMessageRequestSchema, + CreateMessageResultWithToolsSchema, + ElicitationCompleteNotificationSchema, + ElicitRequestSchema, + ElicitResultSchema, + EmptyResultSchema, + GetPromptRequestSchema, + GetPromptResultSchema, + InitializedNotificationSchema, + InitializeRequestSchema, + InitializeResultSchema, + ListPromptsRequestSchema, + ListPromptsResultSchema, + ListResourcesRequestSchema, + ListResourcesResultSchema, + ListResourceTemplatesRequestSchema, + ListResourceTemplatesResultSchema, + ListRootsRequestSchema, + ListRootsResultSchema, + ListToolsRequestSchema, + ListToolsResultSchema, + LoggingMessageNotificationSchema, + PingRequestSchema, + ProgressNotificationSchema, + PromptListChangedNotificationSchema, + ReadResourceRequestSchema, + ReadResourceResultSchema, + ResourceListChangedNotificationSchema, + ResourceUpdatedNotificationSchema, + RootsListChangedNotificationSchema, + SetLevelRequestSchema, + SubscribeRequestSchema, + ToolListChangedNotificationSchema, + UnsubscribeRequestSchema +} from '../../src/types/index.js'; +// Post-relocation home (Q1 increment-2 step 1): the pinned contents are +// unchanged — only the module housing the registries moved. +import { getNotificationSchema, getRequestSchema, getResultSchema } from '../../src/wire/rev2025-11-25/registry.js'; +// The 2025-only task wire vocabulary now lives in the era's schema module +// (Q1 increment-2 step 4); the schema OBJECTS serving the registry are the +// same — these pins still hold by reference. +import { + CancelTaskRequestSchema, + GetTaskPayloadRequestSchema, + GetTaskRequestSchema, + ListTasksRequestSchema, + TaskStatusNotificationSchema +} from '../../src/wire/rev2025-11-25/schemas.js'; + +/** The exact 2025-era request-method → schema map (today's wire surface, verbatim). */ +const EXPECTED_REQUEST_SCHEMAS = { + ping: PingRequestSchema, + initialize: InitializeRequestSchema, + 'completion/complete': CompleteRequestSchema, + 'logging/setLevel': SetLevelRequestSchema, + 'prompts/get': GetPromptRequestSchema, + 'prompts/list': ListPromptsRequestSchema, + 'resources/list': ListResourcesRequestSchema, + 'resources/templates/list': ListResourceTemplatesRequestSchema, + 'resources/read': ReadResourceRequestSchema, + 'resources/subscribe': SubscribeRequestSchema, + 'resources/unsubscribe': UnsubscribeRequestSchema, + 'tools/call': CallToolRequestSchema, + 'tools/list': ListToolsRequestSchema, + 'tasks/get': GetTaskRequestSchema, + 'tasks/result': GetTaskPayloadRequestSchema, + 'tasks/list': ListTasksRequestSchema, + 'tasks/cancel': CancelTaskRequestSchema, + 'sampling/createMessage': CreateMessageRequestSchema, + 'elicitation/create': ElicitRequestSchema, + 'roots/list': ListRootsRequestSchema +} as const; + +/** The exact 2025-era notification-method → schema map. */ +const EXPECTED_NOTIFICATION_SCHEMAS = { + 'notifications/cancelled': CancelledNotificationSchema, + 'notifications/progress': ProgressNotificationSchema, + 'notifications/initialized': InitializedNotificationSchema, + 'notifications/roots/list_changed': RootsListChangedNotificationSchema, + 'notifications/tasks/status': TaskStatusNotificationSchema, + 'notifications/message': LoggingMessageNotificationSchema, + 'notifications/resources/updated': ResourceUpdatedNotificationSchema, + 'notifications/resources/list_changed': ResourceListChangedNotificationSchema, + 'notifications/tools/list_changed': ToolListChangedNotificationSchema, + 'notifications/prompts/list_changed': PromptListChangedNotificationSchema, + 'notifications/elicitation/complete': ElicitationCompleteNotificationSchema +} as const; + +/** + * The exact 2025-era result map (the runtime/typed ALIGNED map — every entry + * is the plain schema `ResultTypeMap` declares; identity-pinned by reference). + */ +const EXPECTED_RESULT_SCHEMAS = { + ping: EmptyResultSchema, + initialize: InitializeResultSchema, + 'completion/complete': CompleteResultSchema, + 'logging/setLevel': EmptyResultSchema, + 'prompts/get': GetPromptResultSchema, + 'prompts/list': ListPromptsResultSchema, + 'resources/list': ListResourcesResultSchema, + 'resources/templates/list': ListResourceTemplatesResultSchema, + 'resources/read': ReadResourceResultSchema, + 'resources/subscribe': EmptyResultSchema, + 'resources/unsubscribe': EmptyResultSchema, + 'tools/call': CallToolResultSchema, + 'tools/list': ListToolsResultSchema, + 'sampling/createMessage': CreateMessageResultWithToolsSchema, + 'elicitation/create': ElicitResultSchema, + 'roots/list': ListRootsResultSchema +} as const; + +/** + * Task methods: served by the request map (2025 wire vocabulary, param-side + * tolerance) but deliberately ABSENT from the result map — `ResultTypeMap` + * excludes them, so the runtime map must too; callers needing task interop + * pass an explicit result schema (the documented overload). + */ +const TASK_REQUEST_METHODS = ['tasks/get', 'tasks/result', 'tasks/list', 'tasks/cancel'] as const; + +/** Methods that must NOT be in the 2025-era registries (2026-only vocabulary). */ +const NOT_IN_2025 = ['server/discover', 'subscriptions/listen', 'notifications/subscriptions/acknowledged'] as const; + +describe('2025-era registry pins (suite-then-move, Q10-L2)', () => { + it('serves exactly the pinned request methods, with the pinned schema objects', () => { + for (const [method, schema] of Object.entries(EXPECTED_REQUEST_SCHEMAS)) { + expect(getRequestSchema(method), method).toBe(schema); + } + }); + + it('serves exactly the pinned notification methods, with the pinned schema objects', () => { + for (const [method, schema] of Object.entries(EXPECTED_NOTIFICATION_SCHEMAS)) { + expect(getNotificationSchema(method), method).toBe(schema); + } + }); + + it('serves the pinned result entries by reference (aligned: plain schemas, no unions)', () => { + for (const [method, schema] of Object.entries(EXPECTED_RESULT_SCHEMAS)) { + expect(getResultSchema(method), method).toBe(schema); + } + }); + + it('serves task requests but has no task result entries (explicit-schema interop)', () => { + for (const method of TASK_REQUEST_METHODS) { + expect(getRequestSchema(method), method).toBeDefined(); + expect(getResultSchema(method), method).toBeUndefined(); + } + }); + + it('returns undefined for non-spec and 2026-only methods', () => { + for (const method of [...NOT_IN_2025, 'acme/custom', 'notifications/acme']) { + expect(getRequestSchema(method), method).toBeUndefined(); + expect(getResultSchema(method), method).toBeUndefined(); + expect(getNotificationSchema(method), method).toBeUndefined(); + } + }); + + it('the registries contain nothing beyond the pinned method sets', () => { + // Completeness guard in the inverse direction: enumerating the maps + // through their module surface must not reveal extra methods. + const requestMethods = Object.keys(EXPECTED_REQUEST_SCHEMAS).sort(); + const notificationMethods = Object.keys(EXPECTED_NOTIFICATION_SCHEMAS).sort(); + const resultMethods = Object.keys(EXPECTED_RESULT_SCHEMAS).sort(); + expect(requestMethods).toHaveLength(20); + expect(notificationMethods).toHaveLength(11); + expect(resultMethods).toHaveLength(16); + // The result-method set is exactly the request-method set minus the + // four task methods (runtime/typed alignment). + expect(resultMethods).toEqual(requestMethods.filter(method => !method.startsWith('tasks/'))); + }); +}); diff --git a/packages/core/test/types/schemaBoundaryPins.test.ts b/packages/core/test/types/schemaBoundaryPins.test.ts new file mode 100644 index 0000000000..a814ef36f8 --- /dev/null +++ b/packages/core/test/types/schemaBoundaryPins.test.ts @@ -0,0 +1,294 @@ +/** + * Behavior-surface pins: the strict/strip/loose line each wire schema draws, + * plus key-existence checks for result members consumers read by name. + * + * The Zod schemas draw a deliberate accept/strip/reject boundary at each layer: + * JSON-RPC envelopes are strict, empty-result acks are strict, typed request + * params strip unknown siblings, and typed results pass unknown siblings + * through to the consumer. An additive protocol revision must not silently + * move that line — these pins make any move loud. A failing pin here means the + * change is deliberate: update the pin together with a changeset and a + * migration-doc entry. + * + * See docs/behavior-surface-pins.md for the maintenance protocol. + */ +import { describe, expect, test } from 'vitest'; + +import { + CallToolRequestSchema, + CallToolResultSchema, + ClientCapabilitiesSchema, + CompleteResultSchema, + EmptyResultSchema, + JSONRPCErrorResponseSchema, + JSONRPCNotificationSchema, + JSONRPCRequestSchema, + JSONRPCResultResponseSchema, + ResultSchema +} from '../../src/types/index.js'; +// The per-request envelope is wire-only vocabulary and now lives in the +// 2026-era wire module (Q1 increment 2); its accept/reject line is unchanged. +import { + ClientCapabilities2026Schema, + ListToolsResultSchema as Wire2026ListToolsResultSchema, + RequestMetaEnvelopeSchema +} from '../../src/wire/rev2026-07-28/schemas.js'; +import type { + CallToolResult, + CompleteResult, + GetPromptResult, + InitializeResult, + ListPromptsResult, + ListResourcesResult, + ListResourceTemplatesResult, + ListToolsResult, + ReadResourceResult, + ServerCapabilities +} from '../../src/types/index.js'; + +/** Extract zod issue codes without depending on zod's generics. */ +const issueCodes = (err: unknown): string[] => ((err as { issues?: Array<{ code: string }> }).issues ?? []).map(i => i.code); + +describe('JSON-RPC envelope schemas are strict', () => { + test('a request with an unknown top-level sibling is rejected', () => { + const parsed = JSONRPCRequestSchema.safeParse({ jsonrpc: '2.0', id: 1, method: 'ping', params: {}, extraTop: true }); + expect(parsed.success).toBe(false); + expect(issueCodes(parsed.error)).toContain('unrecognized_keys'); + }); + + test('a notification with an unknown top-level sibling is rejected', () => { + const parsed = JSONRPCNotificationSchema.safeParse({ jsonrpc: '2.0', method: 'notifications/initialized', extraTop: true }); + expect(parsed.success).toBe(false); + expect(issueCodes(parsed.error)).toContain('unrecognized_keys'); + }); + + test('a result response with an unknown top-level sibling is rejected', () => { + const parsed = JSONRPCResultResponseSchema.safeParse({ jsonrpc: '2.0', id: 1, result: {}, extraTop: true }); + expect(parsed.success).toBe(false); + expect(issueCodes(parsed.error)).toContain('unrecognized_keys'); + }); + + test('an error response with an unknown top-level sibling is rejected', () => { + const parsed = JSONRPCErrorResponseSchema.safeParse({ + jsonrpc: '2.0', + id: 1, + error: { code: -32600, message: 'nope' }, + extraTop: true + }); + expect(parsed.success).toBe(false); + expect(issueCodes(parsed.error)).toContain('unrecognized_keys'); + }); +}); + +describe('EmptyResultSchema is strict', () => { + test('an extra non-declared field rejects', () => { + const parsed = EmptyResultSchema.safeParse({ ok: true }); + expect(parsed.success).toBe(false); + expect(issueCodes(parsed.error)).toContain('unrecognized_keys'); + }); + + test('the declared _meta member is accepted; resultType now rejects (deliberate flip)', () => { + expect(EmptyResultSchema.safeParse({}).success).toBe(true); + expect(EmptyResultSchema.safeParse({ _meta: { note: 'x' } }).success).toBe(true); + // BEHAVIOR MIGRATION (Q1 increment 2, ledgered): `resultType` was cut + // from the base ResultSchema, so the strict empty-result ack now + // REJECTS `{resultType}` bodies at the schema level. On the protocol + // path this is invisible for conforming peers: the era codec consumes + // (2026) or strips (2025, Q1-SD3 ii) the wire member before any + // schema validation runs. Changeset: codec-split-wire-break; + // docs/migration.md "Wire schemas no longer model resultType". + expect(EmptyResultSchema.safeParse({ resultType: 'complete' }).success).toBe(false); + }); +}); + +describe('typed request params strip unknown siblings', () => { + test('an unknown sibling next to declared tools/call params is accepted and stripped', () => { + const parsed = CallToolRequestSchema.parse({ + method: 'tools/call', + params: { name: 'echo', arguments: {}, future2099: 1 } + }); + expect(parsed.params.name).toBe('echo'); + expect('future2099' in parsed.params).toBe(false); + }); +}); + +describe('typed result schemas are loose', () => { + test('the base ResultSchema no longer declares resultType (the masking surface is gone)', () => { + // BEHAVIOR MIGRATION (Q1 increment 2, ledgered): the optional + // `resultType` member that every legacy-leg parse silently accepted + // is cut. The key still passes the loose parse as a FOREIGN sibling + // (guards are consumer-side value checks, not wire validators), but + // no neutral schema declares it; on the protocol path the 2025-era + // codec strips it on lift (Q1-SD3 ii) and the 2026-era codec consumes + // it. Changeset: codec-split-wire-break. + const parsed = ResultSchema.parse({ resultType: 'complete', futureField: 'kept' }); + expect('resultType' in parsed).toBe(true); // loose passthrough, undeclared + expect((parsed as Record).futureField).toBe('kept'); + expect(Object.keys(ResultSchema.shape)).toEqual(['_meta']); + }); + + test('unknown top-level siblings on a tools/call result survive the parse', () => { + const parsed = CallToolResultSchema.parse({ + content: [{ type: 'text', text: 'metered' }], + resultType: 'complete', + ttlMs: 5 + }); + expect(parsed.content).toEqual([{ type: 'text', text: 'metered' }]); + expect((parsed as Record).resultType).toBe('complete'); // undeclared foreign key, loose passthrough + expect((parsed as Record).ttlMs).toBe(5); + }); + + test('CallToolResult requires content on the wire (the silent-empty-success default is gone)', () => { + // BEHAVIOR MIGRATION (Q1 increment 2, ledgered): `content.default([])` + // was removed from the wire schema. The default was the T6 width-leak + // root: a task-shaped (or otherwise content-less) body parsed as a + // silent `{content: []}` success. Content is required by the spec in + // every revision; a content-less body now fails the parse LOUDLY. + // Changeset: codec-split-wire-break; docs/migration.md + // "tools/call results must include content". + expect(CallToolResultSchema.safeParse({ structuredContent: { ok: true } }).success).toBe(false); + const parsed = CallToolResultSchema.parse({ content: [], structuredContent: { ok: true } }); + expect(parsed.content).toEqual([]); + expect(parsed.structuredContent).toEqual({ ok: true }); + }); + + test('CallToolResult preserves isError and sibling members through the parse', () => { + const parsed = CallToolResultSchema.parse({ + content: [{ type: 'text', text: 'ok' }], + structuredContent: { ok: true }, + isError: true, + _meta: { example: 'value' } + }); + expect(parsed.isError).toBe(true); + expect(parsed.structuredContent).toEqual({ ok: true }); + expect(parsed._meta).toEqual({ example: 'value' }); + expect(parsed.content).toEqual([{ type: 'text', text: 'ok' }]); + }); +}); + +describe('completion result boundary', () => { + test('the completion object is loose: unknown sibling fields are preserved', () => { + const parsed = CompleteResultSchema.parse({ completion: { values: ['alpha'], extraField: 'kept' } }); + expect(parsed.completion.values).toEqual(['alpha']); + expect((parsed.completion as Record).extraField).toBe('kept'); + }); + + test('completion.values is capped at 100 entries at the parse boundary', () => { + // The cap is receiver-side ABI: an SDK client cannot observe more than 100 + // values even from a non-SDK server that sends them. + const hundred = Array.from({ length: 100 }, (_, i) => `v${i}`); + expect(CompleteResultSchema.safeParse({ completion: { values: hundred } }).success).toBe(true); + + const overCap = CompleteResultSchema.safeParse({ completion: { values: [...hundred, 'v100'] } }); + expect(overCap.success).toBe(false); + expect(issueCodes(overCap.error)).toContain('too_big'); + }); +}); + +describe('RequestMetaEnvelopeSchema', () => { + const validEnvelope = { + 'io.modelcontextprotocol/protocolVersion': '2026-07-28', + 'io.modelcontextprotocol/clientInfo': { name: 'pin-client', version: '0.0.0' }, + 'io.modelcontextprotocol/clientCapabilities': {} + }; + + test('requires protocolVersion, clientInfo, and clientCapabilities', () => { + expect(RequestMetaEnvelopeSchema.safeParse(validEnvelope).success).toBe(true); + for (const key of Object.keys(validEnvelope)) { + const incomplete: Record = { ...validEnvelope }; + delete incomplete[key]; + expect(RequestMetaEnvelopeSchema.safeParse(incomplete).success).toBe(false); + } + }); + + test('is loose: foreign _meta keys pass through', () => { + const parsed = RequestMetaEnvelopeSchema.parse({ ...validEnvelope, 'com.example/custom': 'kept' }); + expect((parsed as Record)['com.example/custom']).toBe('kept'); + }); + + test('clientCapabilities are validated with the 2026 fork: tasks is not vocabulary on this revision', () => { + // The envelope composes ClientCapabilities2026Schema (the shared + // shape minus the deleted `tasks` key), matching the server-side + // fork wired into DiscoverResultSchema. A tasks-bearing claim is + // foreign vocabulary: it neither validates as a capability (a + // malformed value cannot reject the envelope) nor survives the parse. + const withMalformedTasks = { + ...validEnvelope, + 'io.modelcontextprotocol/clientCapabilities': { tasks: 'not-an-object' } + }; + expect(RequestMetaEnvelopeSchema.safeParse(withMalformedTasks).success).toBe(true); + + const parsed = RequestMetaEnvelopeSchema.parse({ + ...validEnvelope, + 'io.modelcontextprotocol/clientCapabilities': { sampling: {}, tasks: { requests: {} } } + }); + const capabilities = parsed['io.modelcontextprotocol/clientCapabilities'] as Record; + expect(capabilities.sampling).toEqual({}); + expect('tasks' in capabilities).toBe(false); + }); + + test('the 2026 client-capabilities fork tracks the shared shape exactly (minus tasks, by reference)', () => { + // The fork lists its members explicitly (dts-rollup determinism — see + // rev2026-07-28/schemas.ts); this oracle keeps the explicit list from + // drifting: same keys as the shared schema minus `tasks`, and every + // member is the SAME schema object, composed by reference. + const sharedKeys = Object.keys(ClientCapabilitiesSchema.shape).filter(key => key !== 'tasks'); + expect(Object.keys(ClientCapabilities2026Schema.shape)).toEqual(sharedKeys); + for (const key of sharedKeys) { + expect( + (ClientCapabilities2026Schema.shape as Record)[key], + `member '${key}' must be composed by reference from the shared shape` + ).toBe((ClientCapabilitiesSchema.shape as Record)[key]); + } + }); +}); + +describe('2026 wire result members', () => { + test('ttlMs is an integer at the wire boundary (anchor parity: the twin says integer)', () => { + // Type-level parity is structurally blind to this (TS can only say + // `number`), so pin it at the runtime boundary. + const base = { resultType: 'complete', ttlMs: 1500, cacheScope: 'public', tools: [] }; + expect(Wire2026ListToolsResultSchema.safeParse(base).success).toBe(true); + expect(Wire2026ListToolsResultSchema.safeParse({ ...base, ttlMs: 1500.5 }).success).toBe(false); + }); +}); + +// ---- Key-existence checks for consumer-read result members ---- +// +// Mutual-assignability checks against the spec types cannot catch a rename or +// removal of an OPTIONAL member on a loose result type: the old key is absorbed +// by the catchall index signature and the renamed key is optional, so the +// assignment compiles in both directions. Consumers read the members below by +// name, so each must remain a *declared* key of the SDK type. KnownKeyOf strips +// string/number index signatures so that only declared keys count. +type KnownKeyOf = keyof { [K in keyof T as string extends K ? never : number extends K ? never : K]: T[K] }; + +const abiKeys = + () => + & string>(...keys: K[]): K[] => + keys; + +const sdkKeyExistenceChecks = { + CallToolResult: abiKeys()('content', 'structuredContent', 'isError', '_meta'), + InitializeResult: abiKeys()('protocolVersion', 'capabilities', 'serverInfo', 'instructions'), + ServerCapabilities: abiKeys()('experimental', 'completions', 'logging', 'prompts', 'resources', 'tools'), + ListToolsResult: abiKeys()('tools', 'nextCursor'), + ListResourcesResult: abiKeys()('resources', 'nextCursor'), + ListResourceTemplatesResult: abiKeys()('resourceTemplates', 'nextCursor'), + ListPromptsResult: abiKeys()('prompts', 'nextCursor'), + GetPromptResult: abiKeys()('messages'), + ReadResourceResult: abiKeys()('contents'), + CompleteResult: abiKeys()('completion') +}; + +describe('key existence for consumer-read result members', () => { + test('every consumer-read member remains a declared key of its SDK type', () => { + // The compile of `sdkKeyExistenceChecks` above IS the assertion: a renamed + // or removed member fails typecheck. The runtime check guards the table + // itself against accidental truncation. + expect(sdkKeyExistenceChecks.CallToolResult).toEqual(['content', 'structuredContent', 'isError', '_meta']); + for (const keys of Object.values(sdkKeyExistenceChecks)) { + expect(keys.length).toBeGreaterThan(0); + } + }); +}); diff --git a/packages/core/test/types/specTypeSchema.test.ts b/packages/core/test/types/specTypeSchema.test.ts index 198e104f9f..7a077717cf 100644 --- a/packages/core/test/types/specTypeSchema.test.ts +++ b/packages/core/test/types/specTypeSchema.test.ts @@ -90,15 +90,20 @@ describe('isSpecType', () => { } }); - it('narrows to the input type, not the output type, for schemas with defaults', () => { - const v: unknown = {}; + it('CallToolResult requires content at the boundary (the wire default was removed)', () => { + // BEHAVIOR MIGRATION (Q1 increment 2, ledgered): CallToolResultSchema + // lost `content.default([])` — the silent-empty-success masking root. + // The guard's input shape now requires content, matching the spec in + // every revision. Changeset: codec-split-wire-break. + const empty: unknown = {}; + expect(isSpecType.CallToolResult(empty)).toBe(false); + const v: unknown = { content: [] }; expect(isSpecType.CallToolResult(v)).toBe(true); if (isSpecType.CallToolResult(v)) { - // CallToolResultSchema has `content: z.array(...).default([])`, so the input type - // permits `content` to be absent. The guard narrows to that input shape. - expectTypeOf(v.content).toEqualTypeOf(); - expectTypeOf(v).not.toEqualTypeOf(); + expectTypeOf(v.content).toEqualTypeOf(); + expectTypeOf(v.content).not.toEqualTypeOf(); } + void (0 as unknown as CallToolResult); }); it('JSONValue / JSONObject — narrows to the JSON type, not unknown', () => { @@ -134,7 +139,16 @@ describe('SpecTypeName / SpecTypes (type-level)', () => { }); it('SpecTypes[K] matches the named export type', () => { + // RE-SCOPE (Q1 increment 2, ledgered): specTypeSchemas now validate + // the NEUTRAL model. Result entries no longer carry the wire-only + // `resultType` member — the strip-then-equal pin from the public-face + // cut reverts to plain equality, and per-revision wire validators are + // deliberately NOT public surface (addable later via the versioned + // zod-schemas exports). Changeset: codec-split-wire-break. expectTypeOf().toEqualTypeOf(); + type KnownKeys = keyof { [K in keyof T as string extends K ? never : number extends K ? never : K]: T[K] }; + type DeclaresResultType = 'resultType' extends KnownKeys ? true : false; + expectTypeOf().toEqualTypeOf(); expectTypeOf().toEqualTypeOf(); expectTypeOf().toEqualTypeOf(); expectTypeOf().toEqualTypeOf(); diff --git a/packages/core/test/types/wireOnlyHiding.test.ts b/packages/core/test/types/wireOnlyHiding.test.ts new file mode 100644 index 0000000000..1a71e600cc --- /dev/null +++ b/packages/core/test/types/wireOnlyHiding.test.ts @@ -0,0 +1,170 @@ +/** + * Public-face hiding pins: wire-only members and task vocabulary. + * + * Two contracts, enforced at the type level: + * + * 1. Wire-only members are absent from every public result type. `resultType` + * is the 2026-07-28 wire discrimination field; the SDK consumes it at the + * protocol layer and the public types do not declare it. The wire schemas + * keep modeling it internally (also pinned here, so the internal surface + * cannot drift silently either). + * + * 2. Task types are importable, deprecated wire vocabulary that appears in NO + * API signature: the typed method surface (RequestMethod/RequestTypeMap/ + * ResultTypeMap/NotificationTypeMap and everything built on them) offers + * no task method, and the only public declarations naming task types are + * the deprecated vocabulary cluster itself plus the exclusion helpers that + * subtract the task methods from the maps. + */ +import { readFileSync } from 'node:fs'; +import { join } from 'node:path'; + +import { describe, expect, expectTypeOf, test } from 'vitest'; +import type * as z from 'zod/v4'; + +import type { + CallToolResult, + CancelTaskResult, + CompleteResult, + CreateMessageResult, + CreateMessageResultWithTools, + CreateTaskResult, + ElicitResult, + EmptyResult, + GetTaskResult, + InitializeResult, + JSONRPCResultResponse, + ListRootsResult, + ListTasksResult, + ListToolsResult, + NotificationMethod, + ReadResourceResult, + RequestMethod, + RequestTypeMap, + Result, + ResultTypeMap, + Task, + TaskAugmentedRequestParams +} from '../../src/types/types.js'; +import { CallToolResultSchema, ResultSchema } from '../../src/types/schemas.js'; + +/** Declared (non-index-signature) keys of T. */ +type KnownKeyOf = keyof { [K in keyof T as string extends K ? never : number extends K ? never : K]: T[K] }; + +type DeclaresResultType = 'resultType' extends KnownKeyOf ? true : false; + +describe('wire-only members are hidden from the public result types', () => { + test('no public result type declares resultType', () => { + expectTypeOf>().toEqualTypeOf(); + expectTypeOf>().toEqualTypeOf(); + expectTypeOf>().toEqualTypeOf(); + expectTypeOf>().toEqualTypeOf(); + expectTypeOf>().toEqualTypeOf(); + expectTypeOf>().toEqualTypeOf(); + expectTypeOf>().toEqualTypeOf(); + expectTypeOf>().toEqualTypeOf(); + expectTypeOf>().toEqualTypeOf(); + expectTypeOf>().toEqualTypeOf(); + expectTypeOf>().toEqualTypeOf(); + // Deprecated task results are public vocabulary and equally stripped. + expectTypeOf>().toEqualTypeOf(); + expectTypeOf>().toEqualTypeOf(); + // The response envelope embeds the public Result, not the wire shape. + expectTypeOf>().toEqualTypeOf(); + + // Value-assignability is untouched: handler-built results may still + // carry the member through the loose index signature (raw bytes can + // always carry it; the protocol layer owns it). + const handlerBuilt: CallToolResult = { content: [], resultType: 'complete' }; + expect(handlerBuilt).toBeDefined(); + }); + + test('no neutral schema models resultType any more (the masking surface is dead)', () => { + // Q1 increment 2 (ledgered): the shared schema set carried an + // optional resultType on every result parse — the masking surface. + // Post-split, NO neutral schema declares it; the member exists only + // inside the 2026-era wire codec module. Changeset: + // codec-split-wire-break. + expectTypeOf>>().toEqualTypeOf(); + expectTypeOf>>().toEqualTypeOf(); + }); +}); + +describe('task vocabulary is importable but in no API signature', () => { + test('the typed method surface offers no task method', () => { + expectTypeOf>().toEqualTypeOf(); + expectTypeOf>().toEqualTypeOf(); + expectTypeOf>().toEqualTypeOf(); + expectTypeOf>().toEqualTypeOf(); + }); + + test('method-keyed results are plain (no unreachable task members)', () => { + expectTypeOf().toEqualTypeOf(); + expectTypeOf().toEqualTypeOf(); + expectTypeOf().toEqualTypeOf(); + }); + + test('task types stay importable as wire vocabulary', () => { + // The type-only imports above are the proof; spot-check their shapes. + expectTypeOf().toEqualTypeOf(); + expectTypeOf().toEqualTypeOf(); + expectTypeOf>().toEqualTypeOf<'task' | '_meta'>(); + expectTypeOf>().toEqualTypeOf(); + expectTypeOf>().toEqualTypeOf(); + }); + + test('every task type export is tagged @deprecated at the source', () => { + const source = readFileSync(join(__dirname, '..', '..', 'src', 'types', 'types.ts'), 'utf8'); + const taskExports = [...source.matchAll(/export type (\w*Task\w*) /g)].map(match => match[1]); + expect(taskExports.length).toBeGreaterThanOrEqual(17); + for (const name of taskExports) { + const declaration = source.indexOf(`export type ${name} `); + const preceding = source.slice(Math.max(0, declaration - 400), declaration); + expect(preceding, `'${name}' must carry an @deprecated tag`).toContain('@deprecated'); + } + + const guards = readFileSync(join(__dirname, '..', '..', 'src', 'types', 'guards.ts'), 'utf8'); + const guardDecl = guards.indexOf('export const isTaskAugmentedRequestParams'); + expect(guards.slice(Math.max(0, guardDecl - 500), guardDecl)).toContain('@deprecated'); + }); + + test('the task Zod schemas and the related-task meta key carry @deprecated too', () => { + // The migration docs claim the FULL task wire surface is deprecated — + // schemas and constants included, not just the inferred types. The + // task MESSAGE schemas live in the 2025-era wire module since the + // codec split (Q1 increment 2); the param-side carriers stay in the + // neutral file. Both homes are scanned — the combined surface is the + // same ≥19 schemas the docs claim covers. + const neutral = readFileSync(join(__dirname, '..', '..', 'src', 'types', 'schemas.ts'), 'utf8'); + const wire2025 = readFileSync(join(__dirname, '..', '..', 'src', 'wire', 'rev2025-11-25', 'schemas.ts'), 'utf8'); + let total = 0; + for (const schemas of [neutral, wire2025]) { + const schemaExports = [...schemas.matchAll(/export const (\w*Tasks?\w*Schema) /g)].map(match => match[1]); + total += schemaExports.length; + for (const name of schemaExports) { + const declaration = schemas.indexOf(`export const ${name} `); + const preceding = schemas.slice(Math.max(0, declaration - 400), declaration); + expect(preceding, `'${name}' must carry an @deprecated tag`).toContain('@deprecated'); + } + } + expect(total).toBeGreaterThanOrEqual(19); + const schemas = neutral; + + // The `tasks` capability keys on both capability objects. + for (const member of ['tasks: ClientTasksCapabilitySchema.optional()', 'tasks: ServerTasksCapabilitySchema.optional()']) { + const declaration = schemas.indexOf(member); + expect(declaration, `capability member '${member}' must exist`).toBeGreaterThan(-1); + expect(schemas.slice(Math.max(0, declaration - 300), declaration), `'${member}' must carry an @deprecated tag`).toContain( + '@deprecated' + ); + } + + const constants = readFileSync(join(__dirname, '..', '..', 'src', 'types', 'constants.ts'), 'utf8'); + const keyDecl = constants.indexOf('export const RELATED_TASK_META_KEY'); + expect(constants.slice(Math.max(0, keyDecl - 300), keyDecl)).toContain('@deprecated'); + }); +}); + +// A generated-declaration scan (no task type name in any public signature) used +// to live here; the type-level exclusion tests above pin the same contract +// directly against the source types, so the substance stays covered. diff --git a/packages/core/test/wire/eraGates.test.ts b/packages/core/test/wire/eraGates.test.ts new file mode 100644 index 0000000000..97d2ba3313 --- /dev/null +++ b/packages/core/test/wire/eraGates.test.ts @@ -0,0 +1,572 @@ +/** + * Physical deletions through real dispatch (Q1 increment 2). + * + * Era is INSTANCE state: the negotiated protocol version held by the + * Protocol instance selects the wire codec for everything the connection + * sends and receives. Legacy is the default (hand-constructed instances and + * pre-negotiation traffic); modern-era instances get their version set + * through the package-internal hook (`setNegotiatedProtocolVersion`) — the + * same channel the modern-era server entry will use at instance binding. + * + * Registry membership is the deletion story, and these tests prove it at the + * protocol funnels, in both directions: + * + * - inbound: `tasks/get` on a modern-era instance gets −32601 BY ABSENCE — + * even with a handler registered (a custom handler cannot shadow a + * deleted spec method across eras); era-deleted spec notifications are + * silently dropped even with a handler registered. + * - outbound: an era-mismatched spec method dies locally with + * `SdkErrorCode.MethodNotSupportedByProtocolVersion` before anything + * reaches the transport. + * - the 2026 era requires the per-request envelope (−32602 when missing). + * - the stamp seam: 2026-era responses carry `resultType: 'complete'`; + * 2025-era responses NEVER carry it (the 2025 codec has no stamp code + * path — the never-stamp guarantee). + * - encode-side deleted-field strictness (Q1-SD3 iii): `execution` is + * stripped from tools and `tasks` from capability objects on 2026-era + * emissions; both survive untouched on the 2025 era. + * + * `MessageExtraInfo.classification` (INJECTED here; the production + * classifier is the entry/edge's job) no longer selects the era per message: + * the funnel VALIDATES it against the instance era — a mismatch is an + * entry/routing error (typed −32004 rejection / notification drop, plus + * onerror), and unclassified traffic on a legacy instance behaves exactly as + * before the codec split (the B-2 rule). + */ +import { describe, expect, test } from 'vitest'; + +import { SdkError, SdkErrorCode } from '../../src/errors/sdkErrors.js'; +import type { BaseContext } from '../../src/shared/protocol.js'; +import { Protocol, setNegotiatedProtocolVersion } from '../../src/shared/protocol.js'; +import type { JSONRPCMessage, MessageClassification, Result } from '../../src/types/index.js'; +import { InMemoryTransport } from '../../src/util/inMemory.js'; +import * as z from 'zod/v4'; + +class TestProtocol extends Protocol { + protected assertCapabilityForMethod(): void {} + protected assertNotificationCapability(): void {} + protected assertRequestHandlerCapability(): void {} + protected buildContext(ctx: BaseContext): BaseContext { + return ctx; + } +} + +const MODERN: MessageClassification = { era: 'modern', revision: '2026-07-28' }; + +const ENVELOPE = { + 'io.modelcontextprotocol/protocolVersion': '2026-07-28', + 'io.modelcontextprotocol/clientInfo': { name: 'era-client', version: '0.0.0' }, + 'io.modelcontextprotocol/clientCapabilities': {} +}; + +interface Harness { + receiver: TestProtocol; + /** Deliver a raw message to the receiver, optionally classified. */ + deliver: (message: JSONRPCMessage, classification?: MessageClassification) => void; + /** Messages the receiver sent back (responses, notifications). */ + sent: JSONRPCMessage[]; + /** Out-of-band errors surfaced via the receiver's onerror. */ + errors: Error[]; + flush: () => Promise; +} + +interface HarnessOptions { + /** + * Marks the instance's era through the package-internal hook (the same + * channel the modern-era server entry uses at instance binding). Omitted + * = legacy default, exactly like a hand-constructed instance. + */ + era?: '2025-11-25' | '2026-07-28'; + setup?: (receiver: TestProtocol) => void; +} + +async function harness(options: HarnessOptions = {}): Promise { + const [peerTx, receiverTx] = InMemoryTransport.createLinkedPair(); + const sent: JSONRPCMessage[] = []; + peerTx.onmessage = message => void sent.push(message); + await peerTx.start(); + + const receiver = new TestProtocol(); + const errors: Error[] = []; + receiver.onerror = error => void errors.push(error); + options.setup?.(receiver); + if (options.era !== undefined) setNegotiatedProtocolVersion(receiver, options.era); + await receiver.connect(receiverTx); + + return { + receiver, + // Invoke the receiver-side transport callback directly so the test + // controls MessageExtraInfo (the classification handoff seam). + deliver: (message, classification) => receiverTx.onmessage?.(message, classification ? ({ classification } as never) : undefined), + sent, + errors, + flush: () => new Promise(resolve => setTimeout(resolve, 10)) + }; +} + +const errorOf = (msg: JSONRPCMessage | undefined) => (msg as { error?: { code: number; message: string } } | undefined)?.error; +const resultOf = (msg: JSONRPCMessage | undefined) => (msg as { result?: Record } | undefined)?.result; + +describe('inbound era gates — deletions are physical, era is instance state', () => { + const registerTasksGetHandler = (onRun: () => void) => (receiver: TestProtocol) => { + // A custom (3-arg) handler deliberately shadowing the deleted + // spec method: it may serve the 2025 era only. + receiver.setRequestHandler('tasks/get', { params: z.looseObject({ taskId: z.string() }) }, () => { + onRun(); + return {} as Result; + }); + }; + + test('a modern-era instance answers tasks/get with −32601 BY ABSENCE even with a handler registered', async () => { + let handlerRan = false; + const h = await harness({ era: '2026-07-28', setup: registerTasksGetHandler(() => (handlerRan = true)) }); + + // A matching modern classification rides along untouched — the + // handoff check accepts it; the era gate still answers by absence. + h.deliver( + { jsonrpc: '2.0', id: 1, method: 'tasks/get', params: { taskId: 't-1', _meta: { ...ENVELOPE } } } as JSONRPCMessage, + MODERN + ); + await h.flush(); + + expect(handlerRan).toBe(false); + expect(h.sent).toHaveLength(1); + expect(errorOf(h.sent[0])).toMatchObject({ code: -32601, message: 'Method not found' }); + }); + + test('a legacy-era instance (the default) serves tasks/get with that handler — era is fixed per instance', async () => { + let handlerRan = false; + const h = await harness({ setup: registerTasksGetHandler(() => (handlerRan = true)) }); + + // Unclassified, hand-wired instance ⇒ legacy default (B-2): exactly + // the pre-split behavior. + h.deliver({ jsonrpc: '2.0', id: 2, method: 'tasks/get', params: { taskId: 't-1' } } as JSONRPCMessage); + await h.flush(); + + expect(handlerRan).toBe(true); + expect(resultOf(h.sent[0])).toBeDefined(); + }); + + test('ping on a modern-era instance is −32601 by absence (the built-in pong cannot cross eras)', async () => { + const modern = await harness({ era: '2026-07-28' }); + modern.deliver({ jsonrpc: '2.0', id: 3, method: 'ping', params: { _meta: { ...ENVELOPE } } } as JSONRPCMessage, MODERN); + await modern.flush(); + expect(errorOf(modern.sent[0])).toMatchObject({ code: -32601 }); + + // …while a legacy-era instance keeps the automatic pong. + const legacy = await harness(); + legacy.deliver({ jsonrpc: '2.0', id: 4, method: 'ping' } as JSONRPCMessage); + await legacy.flush(); + expect(resultOf(legacy.sent[0])).toEqual({}); + }); + + test('a spec notification the modern era deleted is dropped even with a handler', async () => { + let delivered = 0; + const registerHandler = (receiver: TestProtocol) => { + receiver.setNotificationHandler('notifications/tasks/status', { params: z.looseObject({}) }, () => { + delivered += 1; + }); + }; + + const modern = await harness({ era: '2026-07-28', setup: registerHandler }); + modern.deliver( + { jsonrpc: '2.0', method: 'notifications/tasks/status', params: { taskId: 't', status: 'working' } } as JSONRPCMessage, + MODERN + ); + await modern.flush(); + expect(delivered).toBe(0); + + // Legacy-era instance: delivered. + const legacy = await harness({ setup: registerHandler }); + legacy.deliver({ + jsonrpc: '2.0', + method: 'notifications/tasks/status', + params: { taskId: 't', status: 'working' } + } as JSONRPCMessage); + await legacy.flush(); + expect(delivered).toBe(1); + }); + + test('out-of-universe custom methods stay era-blind (consumer-owned)', async () => { + let served = 0; + const registerHandler = (receiver: TestProtocol) => { + receiver.setRequestHandler('acme/anything', { params: z.looseObject({}) }, () => { + served += 1; + return {} as Result; + }); + }; + + // Served on a modern-era instance (envelope present, as 2026 requires)… + const modern = await harness({ era: '2026-07-28', setup: registerHandler }); + modern.deliver({ jsonrpc: '2.0', id: 5, method: 'acme/anything', params: { _meta: { ...ENVELOPE } } } as JSONRPCMessage, MODERN); + // …and on a legacy-era instance, bare: the era gate never blocks + // methods outside the spec universe on either era. + const legacy = await harness({ setup: registerHandler }); + legacy.deliver({ jsonrpc: '2.0', id: 6, method: 'acme/anything', params: {} } as JSONRPCMessage); + + await modern.flush(); + await legacy.flush(); + expect(served).toBe(2); + }); +}); + +describe('2026-era envelope requiredness at dispatch', () => { + test('a modern-era request without the envelope is −32602 naming the requirement', async () => { + const h = await harness({ + era: '2026-07-28', + setup: receiver => { + receiver.setRequestHandler('tools/list', () => ({ tools: [] })); + } + }); + + h.deliver({ jsonrpc: '2.0', id: 1, method: 'tools/list', params: {} } as JSONRPCMessage, MODERN); + await h.flush(); + + const error = errorOf(h.sent[0]); + expect(error?.code).toBe(-32602); + expect(error?.message).toContain('_meta envelope'); + }); + + test('a modern-era request with a valid envelope is served (handler sees the 2025 shape)', async () => { + const h = await harness({ + era: '2026-07-28', + setup: receiver => { + receiver.setRequestHandler('tools/list', () => ({ tools: [] })); + } + }); + + h.deliver({ jsonrpc: '2.0', id: 2, method: 'tools/list', params: { _meta: { ...ENVELOPE } } } as JSONRPCMessage, MODERN); + await h.flush(); + + expect(resultOf(h.sent[0])).toMatchObject({ tools: [] }); + }); + + test('the 2025 era never requires an envelope', async () => { + const h = await harness({ + setup: receiver => { + receiver.setRequestHandler('tools/list', () => ({ tools: [] })); + } + }); + + h.deliver({ jsonrpc: '2.0', id: 3, method: 'tools/list', params: {} } as JSONRPCMessage); + await h.flush(); + expect(resultOf(h.sent[0])).toMatchObject({ tools: [] }); + }); + + test('−32601 outranks the missing envelope: unknown/era-deleted/unserved methods answer method-not-found', async () => { + // Method existence outranks parameter validity (the canonical + // precedence table for the full inbound validation ladder arrives + // with the validation-ladder milestone; this pins the + // −32601-over-−32602 rule on the modern leg). All three −32601 + // producers win over the envelope −32602: + const h = await harness({ era: '2026-07-28' }); + + // (a) out-of-universe method, no handler registered; + h.deliver({ jsonrpc: '2.0', id: 4, method: 'acme/no-such-method', params: {} } as JSONRPCMessage, MODERN); + // (b) spec method deleted from the era (the era gate runs first); + h.deliver({ jsonrpc: '2.0', id: 5, method: 'tasks/get', params: { taskId: 't-1' } } as JSONRPCMessage, MODERN); + // (c) spec method IN era but with no handler registered. + h.deliver({ jsonrpc: '2.0', id: 6, method: 'tools/list', params: {} } as JSONRPCMessage, MODERN); + await h.flush(); + + expect(h.sent).toHaveLength(3); + for (const message of h.sent) { + expect(errorOf(message)).toMatchObject({ code: -32601, message: 'Method not found' }); + } + }); +}); + +describe('the stamp seam and the never-stamp guarantee', () => { + test('2026-era responses are stamped resultType: complete', async () => { + const h = await harness({ + era: '2026-07-28', + setup: receiver => { + receiver.setRequestHandler('tools/list', () => ({ tools: [] })); + } + }); + + h.deliver({ jsonrpc: '2.0', id: 1, method: 'tools/list', params: { _meta: { ...ENVELOPE } } } as JSONRPCMessage, MODERN); + await h.flush(); + + expect(resultOf(h.sent[0])).toMatchObject({ resultType: 'complete' }); + }); + + test('2025-era responses NEVER carry resultType (no stamp code path exists)', async () => { + const h = await harness({ + setup: receiver => { + receiver.setRequestHandler('tools/list', () => ({ tools: [] })); + } + }); + + h.deliver({ jsonrpc: '2.0', id: 2, method: 'tools/list', params: {} } as JSONRPCMessage); + await h.flush(); + + const result = resultOf(h.sent[0]); + expect(result).toBeDefined(); + expect(result && 'resultType' in result).toBe(false); + }); + + test('the 2025 codec encodeResult is the identity (same reference, nothing added)', async () => { + const { rev2025Codec } = await import('../../src/wire/rev2025-11-25/codec.js'); + const result = { content: [{ type: 'text', text: 'x' }] } as unknown as Result; + expect(rev2025Codec.encodeResult('tools/call', result)).toBe(result); + }); +}); + +describe('encode-side deleted-field strictness (Q1-SD3 iii)', () => { + const TOOL_WITH_EXECUTION = { + name: 'legacy-tool', + inputSchema: { type: 'object' }, + execution: { taskSupport: 'optional' } + }; + + test('execution.taskSupport is stripped from 2026-era tools/list emissions', async () => { + const h = await harness({ + era: '2026-07-28', + setup: receiver => { + receiver.setRequestHandler('tools/list', (() => ({ tools: [TOOL_WITH_EXECUTION] })) as never); + } + }); + + h.deliver({ jsonrpc: '2.0', id: 1, method: 'tools/list', params: { _meta: { ...ENVELOPE } } } as JSONRPCMessage, MODERN); + await h.flush(); + + const tools = resultOf(h.sent[0])?.tools as Array>; + expect(tools[0]).toMatchObject({ name: 'legacy-tool' }); + expect('execution' in tools[0]!).toBe(false); + }); + + test('the same handler emits execution untouched on the 2025 era (era-invisible handlers)', async () => { + const h = await harness({ + setup: receiver => { + receiver.setRequestHandler('tools/list', (() => ({ tools: [TOOL_WITH_EXECUTION] })) as never); + } + }); + + h.deliver({ jsonrpc: '2.0', id: 2, method: 'tools/list', params: {} } as JSONRPCMessage); + await h.flush(); + + const tools = resultOf(h.sent[0])?.tools as Array>; + expect(tools[0]).toMatchObject({ name: 'legacy-tool', execution: { taskSupport: 'optional' } }); + }); + + test('capabilities.tasks is stripped from 2026-era capability-carrying emissions (server/discover)', async () => { + const h = await harness({ + era: '2026-07-28', + setup: receiver => { + receiver.setRequestHandler( + 'server/discover' as never, + (() => ({ + ttlMs: 0, + cacheScope: 'private', + supportedVersions: ['2026-07-28'], + capabilities: { tools: {}, tasks: { list: {} } }, + serverInfo: { name: 's', version: '0' } + })) as never + ); + } + }); + + h.deliver({ jsonrpc: '2.0', id: 3, method: 'server/discover', params: { _meta: { ...ENVELOPE } } } as JSONRPCMessage, MODERN); + await h.flush(); + + const result = resultOf(h.sent[0]); + expect(result).toMatchObject({ resultType: 'complete', capabilities: { tools: {} } }); + expect('tasks' in (result?.capabilities as Record)).toBe(false); + }); +}); + +describe('the edge→instance handoff — classification is validated, never an era switch', () => { + test('a modern-classified request on a legacy-era instance is an entry/routing error: typed −32004, handler never runs', async () => { + let handlerRan = false; + const h = await harness({ + setup: receiver => { + receiver.setRequestHandler('tools/list', () => { + handlerRan = true; + return { tools: [] }; + }); + } + }); + + h.deliver({ jsonrpc: '2.0', id: 1, method: 'tools/list', params: { _meta: { ...ENVELOPE } } } as JSONRPCMessage, MODERN); + await h.flush(); + + expect(handlerRan).toBe(false); + expect(h.sent).toHaveLength(1); + const error = errorOf(h.sent[0]); + expect(error?.code).toBe(-32004); + expect(error?.message).toContain('Unsupported protocol version'); + // Surfaced out of band too: the mismatch is the entry's bug, not the peer's. + expect(h.errors.some(e => e.message.includes('Era mismatch'))).toBe(true); + }); + + test('a legacy-classified request on a modern-era instance is rejected the same way', async () => { + const h = await harness({ + era: '2026-07-28', + setup: receiver => { + receiver.setRequestHandler('tools/list', () => ({ tools: [] })); + } + }); + + h.deliver({ jsonrpc: '2.0', id: 2, method: 'tools/list', params: {} } as JSONRPCMessage, { + era: 'legacy', + revision: '2025-11-25' + }); + await h.flush(); + + expect(errorOf(h.sent[0])).toMatchObject({ code: -32004 }); + expect(h.errors.some(e => e.message.includes('Era mismatch'))).toBe(true); + }); + + test('a modern-classified notification on a legacy-era instance is dropped, with onerror', async () => { + let delivered = 0; + const h = await harness({ + setup: receiver => { + receiver.setNotificationHandler('notifications/progress', () => { + delivered += 1; + }); + } + }); + + h.deliver( + { jsonrpc: '2.0', method: 'notifications/progress', params: { progressToken: 1, progress: 1 } } as JSONRPCMessage, + MODERN + ); + await h.flush(); + + expect(delivered).toBe(0); + expect(h.sent).toHaveLength(0); + expect(h.errors.some(e => e.message.includes('Era mismatch'))).toBe(true); + }); + + test('a matching classification rides along untouched (and unclassified legacy traffic is byte-identical — B-2)', async () => { + const h = await harness({ + setup: receiver => { + receiver.setRequestHandler('tools/list', () => ({ tools: [] })); + } + }); + + // Matching legacy classification. + h.deliver({ jsonrpc: '2.0', id: 3, method: 'tools/list', params: {} } as JSONRPCMessage, { era: 'legacy' }); + // Unclassified (the hand-wired transport posture). + h.deliver({ jsonrpc: '2.0', id: 4, method: 'tools/list', params: {} } as JSONRPCMessage); + await h.flush(); + + expect(h.sent).toHaveLength(2); + expect(resultOf(h.sent[0])).toMatchObject({ tools: [] }); + expect(resultOf(h.sent[1])).toMatchObject({ tools: [] }); + expect(h.errors).toHaveLength(0); + }); +}); + +describe('outbound era gates — typed local error before the transport', () => { + test('a 2026-era instance cannot send 2025-only spec methods', async () => { + const h = await harness({ era: '2026-07-28' }); + + for (const method of ['tasks/get', 'ping', 'logging/setLevel', 'resources/subscribe']) { + const attempt = () => h.receiver.request({ method } as never); + expect(attempt, method).toThrow(SdkError); + try { + attempt(); + } catch (error) { + expect((error as SdkError).code, method).toBe(SdkErrorCode.MethodNotSupportedByProtocolVersion); + expect((error as SdkError).data, method).toMatchObject({ method, era: '2026-07-28' }); + } + } + // Nothing reached the transport. + expect(h.sent).toHaveLength(0); + }); + + test('a legacy-era instance cannot send server/discover', async () => { + const h = await harness({ era: '2025-11-25' }); + + expect(() => h.receiver.request({ method: 'server/discover' } as never)).toThrow(SdkError); + try { + h.receiver.request({ method: 'server/discover' } as never); + } catch (error) { + expect((error as SdkError).code).toBe(SdkErrorCode.MethodNotSupportedByProtocolVersion); + } + expect(h.sent).toHaveLength(0); + }); + + test('outbound era-mismatched spec notifications die locally too', async () => { + const h = await harness({ era: '2026-07-28' }); + + await expect(h.receiver.notification({ method: 'notifications/roots/list_changed' })).rejects.toMatchObject({ + code: SdkErrorCode.MethodNotSupportedByProtocolVersion + }); + expect(h.sent).toHaveLength(0); + }); + + test('pre-negotiation bootstrap pins still route initialize to the 2025 era', async () => { + // An instance with NO negotiated version may always send the legacy + // handshake; setting a modern version afterwards closes it (the pin + // applies only while the negotiated version is unset — a negotiated + // session never re-routes onto the other era). + const h = await harness(); + const pending = h.receiver.request({ + method: 'initialize', + params: { protocolVersion: '2025-11-25', capabilities: {}, clientInfo: { name: 'c', version: '0' } } + }); + pending.catch(() => undefined); // unanswered; we only assert the send happened + await h.flush(); + // The handshake reached the wire (sent[] captures the peer's inbox). + expect(h.sent).toHaveLength(1); + expect((h.sent[0] as { method?: string }).method).toBe('initialize'); + await h.receiver.close(); + + const h2 = await harness({ era: '2026-07-28' }); + expect(() => + h2.receiver.request({ + method: 'initialize', + params: { protocolVersion: '2025-11-25', capabilities: {}, clientInfo: { name: 'c', version: '0' } } + }) + ).toThrow(SdkError); + }); +}); + +describe('T6 width-leak killed at both roots', () => { + test('2026 era: a task-shaped tools/call body can never parse as an empty success', async () => { + const { rev2026Codec } = await import('../../src/wire/rev2026-07-28/codec.js'); + // resultType present-and-complete but the body is task-shaped: the + // wire-exact parse requires content — loud invalid, never {content: []}. + const decoded = rev2026Codec.decodeResult('tools/call', { + resultType: 'complete', + task: { taskId: 't-1', status: 'working' } + }); + expect(decoded.kind).toBe('invalid'); + }); + + test('2025 era: with the content default gone, a bare task-shaped body fails the plain schema loudly', async () => { + const { rev2025Codec } = await import('../../src/wire/rev2025-11-25/codec.js'); + const { CallToolResultSchema } = await import('../../src/types/schemas.js'); + const decoded = rev2025Codec.decodeResult('tools/call', { task: { taskId: 't-1', status: 'working' } }); + expect(decoded.kind).toBe('complete'); + if (decoded.kind === 'complete') { + // The plain schema (which IS the registry entry — the result map + // is aligned to the typed map, no task-widened union): no + // default([]) means no silent {content: []} masking. + expect(CallToolResultSchema.safeParse(decoded.result).success).toBe(false); + } + // The GENERIC path agrees: the registry serves the same plain schema, + // so even a fully conforming CreateTaskResult body is a loud schema + // failure (surfaced as a typed INVALID_RESULT — see + // test/shared/typedMapAlignment.test.ts). Task interop is the + // explicit-schema overload, never a silent union member. + const { getResultSchema } = await import('../../src/wire/rev2025-11-25/registry.js'); + const plain = getResultSchema('tools/call'); + expect(plain).toBe(CallToolResultSchema); + expect( + plain!.safeParse({ + task: { + taskId: '786af6b0-2779-48ed-9cc1-b8a8a25b8a86', + status: 'working', + createdAt: '2025-11-25T10:30:00Z', + lastUpdatedAt: '2025-11-25T10:30:05Z', + ttl: 60000, + pollInterval: 5000 + } + }).success + ).toBe(false); + }); +}); diff --git a/packages/core/test/wire/neutralKeyParity.test.ts b/packages/core/test/wire/neutralKeyParity.test.ts new file mode 100644 index 0000000000..316513541b --- /dev/null +++ b/packages/core/test/wire/neutralKeyParity.test.ts @@ -0,0 +1,98 @@ +/** + * The neutralKeys pin family (Q1 increment 3): + * + * neutralKeys(T) = wireKeys@rev(T) − WIRE_ONLY + * + * For every mapped result type, the NEUTRAL public type's declared keys must + * equal the revision's WIRE type's declared keys minus the wire-only set + * (`resultType` — the envelope keys and retry fields are params-side and + * never appear on result types). This closes BOTH inherited verification + * holes at once: + * - the old 2025 suite tolerated a phantom `resultType` key on every result + * (`AssertExactKeysWithResultType`), and + * - the old 2026 suite had no key parity at all. + * + * OWNED PENDING DELTA (stale-checked): the 2026 cacheable results carry + * `ttlMs`/`cacheScope` on the wire. Those are CONSUMER-RELEVANT (cache fields + * are deliberately NOT wire-only — Q13) but the neutral model does not carry + * them until the cache-hint surface lands (M3.2/#12). Each cacheable entry + * below subtracts them explicitly; when M3.2 models them neutrally, the + * subtraction breaks the build and the entry burns. + */ +import { describe, expect, test } from 'vitest'; +import type * as z4 from 'zod/v4'; + +import type * as SDK from '../../src/types/index.js'; +import type * as Wire2026 from '../../src/wire/rev2026-07-28/schemas.js'; + +/* eslint-disable @typescript-eslint/no-unused-vars */ + +type KnownKeys = keyof { [K in keyof T as string extends K ? never : number extends K ? never : K]: T[K] }; + +type AssertSameKeys = [KnownKeys] extends [KnownKeys] + ? [KnownKeys] extends [KnownKeys] + ? true + : { _brand: 'KeyMismatch'; missingFromA: Exclude, KnownKeys> } + : { _brand: 'KeyMismatch'; extraInA: Exclude, KnownKeys> }; + +type Assert = T; + +/** The wire-only key set on results (the hide set's result-side member). */ +type WIRE_ONLY = 'resultType'; + +/** M3.2-owned pending delta: cache fields modeled on the wire, not yet neutrally. */ +type M32_PENDING = 'ttlMs' | 'cacheScope'; + +type MinusWireOnly = { [K in keyof T as K extends WIRE_ONLY ? never : K]: T[K] }; +type MinusWireOnlyAndCache = { [K in keyof T as K extends WIRE_ONLY | M32_PENDING ? never : K]: T[K] }; + +/* ---- 2026: neutralKeys(T) = wireKeys@2026(T) − WIRE_ONLY ---- */ + +type _N26_Result = Assert>>>; +type _N26_EmptyResult = Assert>>>; +type _N26_CallToolResult = Assert>>>; +type _N26_CompleteResult = Assert>>>; +type _N26_GetPromptResult = Assert>>>; +// Cacheable results: ttlMs/cacheScope subtracted until M3.2 models them neutrally. +type _N26_ListToolsResult = Assert< + AssertSameKeys>> +>; +type _N26_ListPromptsResult = Assert< + AssertSameKeys>> +>; +type _N26_ListResourcesResult = Assert< + AssertSameKeys>> +>; +type _N26_ListResourceTemplatesResult = Assert< + AssertSameKeys>> +>; +type _N26_ReadResourceResult = Assert< + AssertSameKeys>> +>; +type _N26_DiscoverResult = Assert< + AssertSameKeys>> +>; + +/* ---- 2025: the wire schemas ARE the neutral schemas post-cut — pin that no + * result type re-grows a resultType slot (the masking surface stays dead). ---- */ + +type DeclaresResultType = 'resultType' extends KnownKeys ? true : false; +type _N25_Result = Assert extends false ? true : false>; +type _N25_EmptyResult = Assert extends false ? true : false>; +type _N25_CallToolResult = Assert extends false ? true : false>; +type _N25_InitializeResult = Assert extends false ? true : false>; +type _N25_CreateMessageResult = Assert extends false ? true : false>; +type _N25_ElicitResult = Assert extends false ? true : false>; +type _N25_ListRootsResult = Assert extends false ? true : false>; +type _N25_GetTaskResult = Assert extends false ? true : false>; +type _N25_ClientResult = Assert extends false ? true : false>; +type _N25_ServerResult = Assert extends false ? true : false>; + +describe('neutralKeys pin family', () => { + test('the compile of this file IS the assertion (runtime guard against truncation)', () => { + // 11 per-type 2026 pins + 10 resultType-absence pins are enforced at + // type level above; this runtime test exists so the file cannot be + // silently excluded from the suite. + expect(true).toBe(true); + }); +}); diff --git a/packages/core/test/wire/registryDiffOracle.test.ts b/packages/core/test/wire/registryDiffOracle.test.ts new file mode 100644 index 0000000000..c782e16664 --- /dev/null +++ b/packages/core/test/wire/registryDiffOracle.test.ts @@ -0,0 +1,104 @@ +/** + * Registry-diff oracle (Q1 increment 3 — generation as ORACLE, never source). + * + * The per-era method registries are HAND-WRITTEN (a generator walking anchor + * method literals would silently re-admit the 2026-demoted server→client + * methods — the flavor-(b) trap). This oracle derives each revision's method + * universe FROM THE ANCHOR SOURCE at test time and fails LOUD — with the + * exact diff — whenever the anchor and the hand registry disagree, modulo a + * documented seed-decision list that is stale-checked in both directions. + * + * Seed decisions (every entry is a deliberate, owned divergence): + * - 2026 DEMOTIONS: `sampling/createMessage`, `elicitation/create`, + * `roots/list` keep method literals in the anchor but are NOT wire request + * methods in 2026 — the server→client JSON-RPC request channel is deleted + * (`ServerRequest` has no 2026 export; the shapes survive only as in-band + * `InputRequest` payloads, M4.1/#13). + * - 2026 DEFERRALS: `subscriptions/listen` and + * `notifications/subscriptions/acknowledged` are real 2026 wire methods + * whose SHELLS land with the subscriptions feature (M6.1/#14). The day #14 + * wires them, this oracle fails until the entries are removed — that + * failure is the burn-down notification, by design. + */ +import fs from 'node:fs'; +import path from 'node:path'; + +import { describe, expect, test } from 'vitest'; + +import { rev2025NotificationMethods, rev2025RequestMethods } from '../../src/wire/rev2025-11-25/registry.js'; +import { rev2026NotificationMethods, rev2026RequestMethods } from '../../src/wire/rev2026-07-28/registry.js'; + +const ANCHORS = { + '2025-11-25': path.resolve(__dirname, '../../src/types/spec.types.2025-11-25.ts'), + '2026-07-28': path.resolve(__dirname, '../../src/types/spec.types.2026-07-28.ts') +} as const; + +/** Extract every `method: ''` from an anchor source. */ +function anchorMethods(revision: keyof typeof ANCHORS): { requests: string[]; notifications: string[] } { + const source = fs.readFileSync(ANCHORS[revision], 'utf8'); + const literals = [...source.matchAll(/method:\s*'([^']+)'/g)].map(m => m[1]!); + const unique = [...new Set(literals)].sort(); + return { + requests: unique.filter(m => !m.startsWith('notifications/')), + notifications: unique.filter(m => m.startsWith('notifications/')) + }; +} + +/** Anchor-side methods deliberately NOT in the hand registry (reason per entry). */ +const SEED_EXCLUSIONS: Record> = { + '2025-11-25': {}, + '2026-07-28': { + 'sampling/createMessage': 'DEMOTED to an in-band InputRequest payload (M4.1/#13) — not a 2026 wire request', + 'elicitation/create': 'DEMOTED to an in-band InputRequest payload (M4.1/#13) — not a 2026 wire request', + 'roots/list': 'DEMOTED to an in-band InputRequest payload (M4.1/#13) — not a 2026 wire request', + 'subscriptions/listen': 'DEFERRED to the subscriptions feature (M6.1/#14)', + 'notifications/subscriptions/acknowledged': 'DEFERRED to the subscriptions feature (M6.1/#14)' + } +}; + +const REGISTRIES = { + '2025-11-25': { requests: rev2025RequestMethods, notifications: rev2025NotificationMethods }, + '2026-07-28': { requests: rev2026RequestMethods, notifications: rev2026NotificationMethods } +} as const; + +describe.each(['2025-11-25', '2026-07-28'] as const)('registry-diff oracle %s', revision => { + const anchor = anchorMethods(revision); + const registry = REGISTRIES[revision]; + const exclusions = SEED_EXCLUSIONS[revision]!; + + test('every anchor method is in the hand registry or a documented seed exclusion', () => { + const missing = [...anchor.requests, ...anchor.notifications].filter(method => { + const inRegistry = registry.requests.includes(method) || registry.notifications.includes(method); + return !inRegistry && !(method in exclusions); + }); + expect( + missing, + `Anchor methods absent from the ${revision} registry with NO seed decision — ` + + `wire them or add a documented exclusion (this is the loud failure the oracle exists for)` + ).toEqual([]); + }); + + test('the hand registry contains nothing beyond the anchor universe', () => { + const anchorSet = new Set([...anchor.requests, ...anchor.notifications]); + const extra = [...registry.requests, ...registry.notifications].filter(method => !anchorSet.has(method)); + expect(extra, `Registry methods with no ${revision} anchor literal — era leak or typo`).toEqual([]); + }); + + test('seed exclusions are not stale (still in the anchor, still not in the registry)', () => { + for (const [method, reason] of Object.entries(exclusions)) { + const inAnchor = anchor.requests.includes(method) || anchor.notifications.includes(method); + expect(inAnchor, `${method}: exclusion no longer matches any anchor literal — remove it (${reason})`).toBe(true); + const inRegistry = registry.requests.includes(method) || registry.notifications.includes(method); + expect(inRegistry, `${method}: now wired in the registry — remove the stale exclusion (${reason})`).toBe(false); + } + }); + + test('the anchor universe is fully partitioned (sanity: counts add up)', () => { + const total = anchor.requests.length + anchor.notifications.length; + const covered = + registry.requests.filter(m => anchor.requests.includes(m)).length + + registry.notifications.filter(m => anchor.notifications.includes(m)).length + + Object.keys(exclusions).length; + expect(covered).toBe(total); + }); +}); diff --git a/packages/core/test/wire/schemaTwinConformance.test.ts b/packages/core/test/wire/schemaTwinConformance.test.ts new file mode 100644 index 0000000000..e0f4b21dca --- /dev/null +++ b/packages/core/test/wire/schemaTwinConformance.test.ts @@ -0,0 +1,126 @@ +/** + * Schema-twin conformance lock (Q1 increment 3 — generation as ORACLE). + * + * The spec repository generates `schema.json` from the same normative + * `schema.ts` the anchors vendor. The twins vendored under + * `corpus/schema-twins/` (TEST-ONLY — never bundled, never runtime; the + * engines stay optional peers and the hot path stays hand-written Zod) give + * a generated, revision-exact validator for every named spec type. This + * suite locks the hand-written wire layer to them, per revision per fixture: + * + * - every accept-corpus fixture must satisfy the GENERATED validator for its + * directory's spec type (catches twin/anchor desync and hand-corpus drift + * — the 2025 mini-corpus is hand-built, so this is its only independent + * referee), and + * - every fixture the SDK wire layer accepts must also be twin-valid + * (agreement on the accept side; reject-side deltas are owned by the + * dispatch-routed rejection corpus, since generated valid-only oracles are + * blind to them). + * + * Twin refresh is ATOMIC with the matching anchor (lifecycle rule 4, + * packages/core/src/types/README.md); provenance in schema-twins/manifest.json. + */ +import { createHash } from 'node:crypto'; +import { readdirSync, readFileSync, statSync } from 'node:fs'; +import { join } from 'node:path'; + +import { Ajv2020 as Ajv } from 'ajv/dist/2020.js'; +import addFormats from 'ajv-formats'; +import { describe, expect, test } from 'vitest'; + +const FIXTURES_ROOT = join(__dirname, '../corpus/fixtures'); +const TWINS_ROOT = join(__dirname, '../corpus/schema-twins'); + +interface TwinManifest { + source: { repository: string; commit: string }; + files: Record; +} + +const TWIN_MANIFEST = JSON.parse(readFileSync(join(TWINS_ROOT, 'manifest.json'), 'utf8')) as TwinManifest; + +describe('twin provenance integrity (the manifest lock)', () => { + // The twins' authority as generated oracles rests on them being the raw + // upstream artifacts, byte for byte. Hash the vendored files against the + // manifest's provenance values at test time so ANY rewrite — prettier, an + // editor, a manual touch-up — fails loudly. Refresh only via + // `pnpm fetch:schema-twins` (which recomputes these values from the + // fetched bytes), atomically with the matching spec.types anchor. + test.each(Object.keys(TWIN_MANIFEST.files))('%s twin is byte-identical to the upstream artifact pinned in the manifest', revision => { + const entry = TWIN_MANIFEST.files[revision]!; + const raw = readFileSync(join(TWINS_ROOT, `${revision}.schema.json`)); + expect(raw.byteLength, `byte size drifted for ${revision} — the vendored twin was rewritten`).toBe(entry.bytes); + expect( + createHash('sha256').update(raw).digest('hex'), + `sha256 drifted for ${revision} — the vendored twin was rewritten (re-vendor raw bytes via pnpm fetch:schema-twins)` + ).toBe(entry.sha256); + }); +}); + +type JsonSchema = { $defs?: Record }; + +function twinValidatorFactory(revision: string) { + const schema = JSON.parse(readFileSync(join(TWINS_ROOT, `${revision}.schema.json`), 'utf8')) as JsonSchema; + const ajv = new Ajv({ strict: false, allowUnionTypes: true }); + addFormats.default ? addFormats.default(ajv) : (addFormats as unknown as (a: Ajv) => void)(ajv); + ajv.addSchema(schema, 'spec'); + return { + defs: new Set(Object.keys(schema.$defs ?? {})), + requiredOf(typeName: string): string[] { + return schema.$defs?.[typeName]?.required ?? []; + }, + validatorFor(typeName: string) { + return ajv.getSchema(`spec#/$defs/${typeName}`); + } + }; +} + +function listTypeDirs(revision: string): string[] { + const root = join(FIXTURES_ROOT, revision); + return readdirSync(root) + .filter(entry => statSync(join(root, entry)).isDirectory()) + .sort(); +} + +function listFixtures(revision: string, dir: string): string[] { + return readdirSync(join(FIXTURES_ROOT, revision, dir)) + .filter(file => file.endsWith('.json')) + .sort(); +} + +describe.each(['2025-11-25', '2026-07-28'] as const)('schema-twin conformance lock %s', revision => { + const twin = twinValidatorFactory(revision); + const dirs = listTypeDirs(revision).filter(dir => twin.defs.has(dir)); + + test('the twin covers the corpus (the unmapped set is pinned exactly)', () => { + const unmapped = listTypeDirs(revision).filter(dir => !twin.defs.has(dir)); + // Unmapped directories would be SDK-named shapes with no spec def. + // Today there are NONE — the set is pinned exactly, not bounded with + // slack: a new unmapped directory means the twin and the corpus are + // drifting apart and must be adjudicated here by name. + expect(unmapped).toEqual([]); + expect(dirs.length).toBeGreaterThan(30); + }); + + describe.each(dirs)('%s', dir => { + test.each(listFixtures(revision, dir))('%s satisfies the generated spec validator', file => { + let fixture = JSON.parse(readFileSync(join(FIXTURES_ROOT, revision, dir, file), 'utf8')) as Record; + // The hand-built 2025 mini-corpus stores BARE message shapes (the + // SDK parse surface); the spec defs model the full JSON-RPC wire + // message. Supply the neutral envelope members the def requires + // and the fixture deliberately omits — the PAYLOAD is what the + // fixtures pin, and it crosses to the twin verbatim. + const required = twin.requiredOf(dir); + if (typeof fixture === 'object' && fixture !== null && !('jsonrpc' in fixture)) { + if (required.includes('jsonrpc')) fixture = { jsonrpc: '2.0', ...fixture }; + if (required.includes('id') && !('id' in fixture)) fixture = { id: 'twin-probe', ...fixture }; + } + const validate = twin.validatorFor(dir); + expect(validate, `no compiled validator for ${dir}`).toBeDefined(); + const valid = validate!(fixture); + expect( + valid, + `'${dir}/${file}' rejected by the generated ${revision} validator:\n${JSON.stringify(validate!.errors, null, 2)}` + ).toBe(true); + }); + }); +}); diff --git a/packages/server/src/server/server.ts b/packages/server/src/server/server.ts index f96d8ec1bc..a82432d968 100644 --- a/packages/server/src/server/server.ts +++ b/packages/server/src/server/server.ts @@ -9,6 +9,7 @@ import type { ElicitRequestFormParams, ElicitRequestURLParams, ElicitResult, + EmptyResult, Implementation, InitializeRequest, InitializeResult, @@ -16,6 +17,7 @@ import type { JsonSchemaType, jsonSchemaValidator, ListRootsRequest, + ListRootsResult, LoggingLevel, LoggingMessageNotification, MessageExtraInfo, @@ -32,22 +34,20 @@ import type { ToolUseContent } from '@modelcontextprotocol/core'; import { - CallToolRequestSchema, - CallToolResultSchema, + codecForVersion, CreateMessageResultSchema, CreateMessageResultWithToolsSchema, - ElicitResultSchema, - EmptyResultSchema, LATEST_PROTOCOL_VERSION, - ListRootsResultSchema, LoggingLevelSchema, mergeCapabilities, + negotiatedProtocolVersionOf, parseSchema, Protocol, ProtocolError, ProtocolErrorCode, SdkError, - SdkErrorCode + SdkErrorCode, + setNegotiatedProtocolVersion } from '@modelcontextprotocol/core'; import { DefaultJsonSchemaValidator } from '@modelcontextprotocol/server/_shims'; @@ -90,7 +90,6 @@ export type ServerOptions = ProtocolOptions & { export class Server extends Protocol { private _clientCapabilities?: ClientCapabilities; private _clientVersion?: Implementation; - private _negotiatedProtocolVersion?: string; private _capabilities: ServerCapabilities; private _instructions?: string; private _jsonSchemaValidator: jsonSchemaValidator; @@ -205,7 +204,19 @@ export class Server extends Protocol { return handler; } return async (request, ctx) => { - const validatedRequest = parseSchema(CallToolRequestSchema, request); + // Era-exact validation: the request and result schemas come from + // the instance era, resolved at dispatch time (the era gate + // guarantees tools/call exists on the serving era). + const codec = codecForVersion(negotiatedProtocolVersionOf(this)); + const callToolRequestSchema = codec.requestSchema('tools/call'); + // The era registry entry IS the plain CallToolResult schema (the + // result map is aligned to the typed map — no widened unions), + // so no narrower surface is needed. + const callToolResultSchema = codec.resultSchema('tools/call'); + if (!callToolRequestSchema || !callToolResultSchema) { + throw new ProtocolError(ProtocolErrorCode.InternalError, 'No wire schema for tools/call in the resolved era'); + } + const validatedRequest = parseSchema(callToolRequestSchema, request); if (!validatedRequest.success) { const errorMessage = validatedRequest.error instanceof Error ? validatedRequest.error.message : String(validatedRequest.error); @@ -214,7 +225,7 @@ export class Server extends Protocol { const result = await handler(request, ctx); - const validationResult = parseSchema(CallToolResultSchema, result); + const validationResult = parseSchema(callToolResultSchema, result); if (!validationResult.success) { const errorMessage = validationResult.error instanceof Error ? validationResult.error.message : String(validationResult.error); @@ -379,7 +390,10 @@ export class Server extends Protocol { ? requestedVersion : (this._supportedProtocolVersions[0] ?? LATEST_PROTOCOL_VERSION); - this._negotiatedProtocolVersion = protocolVersion; + // The negotiated version is the instance's connection state — it IS + // the wire-era selection for everything this instance sends and + // receives from here on (legacy handshake ⇒ a legacy-era version). + setNegotiatedProtocolVersion(this, protocolVersion); this.transport?.setProtocolVersion?.(protocolVersion); return { @@ -410,7 +424,7 @@ export class Server extends Protocol { * `undefined` before initialization. */ getNegotiatedProtocolVersion(): string | undefined { - return this._negotiatedProtocolVersion; + return negotiatedProtocolVersionOf(this); } /** @@ -420,8 +434,8 @@ export class Server extends Protocol { return this._capabilities; } - async ping() { - return this._requestWithSchema({ method: 'ping' }, EmptyResultSchema); + async ping(): Promise { + return this.request({ method: 'ping' }); } /** @@ -511,11 +525,16 @@ export class Server extends Protocol { } } - // Use different schemas based on whether tools are provided + // Use different schemas based on whether tools are provided. The + // result schema depends on the REQUEST params, which a method-keyed + // registry entry cannot express, so it goes through the explicit- + // schema path (still era-gated: sampling/createMessage is not a wire + // request on the 2026 era, so a modern-era instance fails with the + // typed era error before anything reaches the transport). if (params.tools) { - return this._requestWithSchema({ method: 'sampling/createMessage', params }, CreateMessageResultWithToolsSchema, options); + return await this._requestWithSchema({ method: 'sampling/createMessage', params }, CreateMessageResultWithToolsSchema, options); } - return this._requestWithSchema({ method: 'sampling/createMessage', params }, CreateMessageResultSchema, options); + return await this._requestWithSchema({ method: 'sampling/createMessage', params }, CreateMessageResultSchema, options); } /** @@ -535,7 +554,9 @@ export class Server extends Protocol { } const urlParams = params as ElicitRequestURLParams; - return this._requestWithSchema({ method: 'elicitation/create', params: urlParams }, ElicitResultSchema, options); + // Method-keyed request(): the era registry's plain + // ElicitResult schema is exactly the narrow surface. + return this.request({ method: 'elicitation/create', params: urlParams }, options); } case 'form': { if (!this._clientCapabilities?.elicitation?.form) { @@ -545,11 +566,7 @@ export class Server extends Protocol { const formParams: ElicitRequestFormParams = params.mode === 'form' ? (params as ElicitRequestFormParams) : { ...(params as ElicitRequestFormParams), mode: 'form' }; - const result = await this._requestWithSchema( - { method: 'elicitation/create', params: formParams }, - ElicitResultSchema, - options - ); + const result = await this.request({ method: 'elicitation/create', params: formParams }, options); if (result.action === 'accept' && result.content && formParams.requestedSchema) { try { @@ -612,8 +629,8 @@ export class Server extends Protocol { * Remains functional during the deprecation window (at least twelve months). * Migrate to passing paths via tool parameters, resource URIs, or configuration. */ - async listRoots(params?: ListRootsRequest['params'], options?: RequestOptions) { - return this._requestWithSchema({ method: 'roots/list', params }, ListRootsResultSchema, options); + async listRoots(params?: ListRootsRequest['params'], options?: RequestOptions): Promise { + return this.request({ method: 'roots/list', params }, options); } /** diff --git a/packages/server/test/server/server.test.ts b/packages/server/test/server/server.test.ts index 0edcfd3af0..4ca198535b 100644 --- a/packages/server/test/server/server.test.ts +++ b/packages/server/test/server/server.test.ts @@ -1,4 +1,4 @@ -import type { JSONRPCMessage, JSONRPCRequest } from '@modelcontextprotocol/core'; +import type { CallToolResult, JSONRPCMessage, JSONRPCRequest } from '@modelcontextprotocol/core'; import { InitializeResultSchema, InMemoryTransport, @@ -129,5 +129,92 @@ describe('Server', () => { await server.close(); }); + + it('counter-offers only released versions when a draft revision is requested', async () => { + // ORDERING PIN — counter-offer leak guard. The initialize + // counter-offer is `supportedProtocolVersions[0]`: whatever sits at + // the head of that list is offered to EVERY legacy-era client whose + // requested version is unknown. Era-aware supported-version list + // semantics must therefore land BEFORE any LATEST/SUPPORTED + // constant bump that adds a 2026-era revision — bumping first + // would leak the modern revision into 2025-era initialize + // handshakes via this exact site. If this pin goes red because the + // constants moved, do NOT update it until the counter-offer is + // era-aware. + const DRAFT_REVISION = '2026-07-28'; + expect(SUPPORTED_PROTOCOL_VERSIONS).not.toContain(DRAFT_REVISION); + const server = new Server({ name: 'test', version: '1.0.0' }, { capabilities: {} }); + + const respondedVersion = await initializeServer(server, DRAFT_REVISION); + + expect(respondedVersion).toBe(LATEST_PROTOCOL_VERSION); + expect(respondedVersion).not.toBe(DRAFT_REVISION); + expect(server.getNegotiatedProtocolVersion()).toBe(LATEST_PROTOCOL_VERSION); + + await server.close(); + }); + }); + + describe('tools/call handler-result validation (required content)', () => { + // Server-side pin for the documented wire break (docs/migration.md, + // "CallToolResult.content … required at the wire boundary"): with the + // content.default([]) affordance removed, a handler result without + // `content` is rejected with -32602 `Invalid tools/call result` — + // never silently defaulted onto the wire — while an authored-content + // result passes through the wrapped handler untouched. + async function callToolOnServer(result: CallToolResult): Promise { + const server = new Server({ name: 'test', version: '1.0.0' }, { capabilities: { tools: {} } }); + server.setRequestHandler('tools/call', () => result); + + const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); + const received: JSONRPCMessage[] = []; + clientTransport.onmessage = message => void received.push(message); + await server.connect(serverTransport); + await clientTransport.start(); + + await clientTransport.send({ + jsonrpc: '2.0', + id: 1, + method: 'initialize', + params: { + protocolVersion: LATEST_PROTOCOL_VERSION, + capabilities: {}, + clientInfo: { name: 'test-client', version: '1.0.0' } + } + }); + await clientTransport.send({ jsonrpc: '2.0', method: 'notifications/initialized' }); + await clientTransport.send({ jsonrpc: '2.0', id: 2, method: 'tools/call', params: { name: 'echo', arguments: {} } }); + await new Promise(resolve => setTimeout(resolve, 10)); + await server.close(); + + const response = received.find(message => (message as { id?: unknown }).id === 2); + if (!response) { + throw new Error('no tools/call response received'); + } + return response; + } + + it('rejects a structured-only handler result (no content) with -32602 Invalid tools/call result', async () => { + const response = await callToolOnServer({ structuredContent: { ok: true } } as unknown as CallToolResult); + + const error = (response as { error?: { code: number; message: string } }).error; + expect(error).toBeDefined(); + expect(error!.code).toBe(-32602); + expect(error!.message).toContain('Invalid tools/call result'); + }); + + it('passes an authored-content result through to the wire', async () => { + const response = await callToolOnServer({ + content: [{ type: 'text', text: 'hi' }], + structuredContent: { ok: true } + }); + + if (!isJSONRPCResultResponse(response)) { + throw new Error(`Expected a result response, got: ${JSON.stringify(response)}`); + } + const result = response.result as { content: unknown; structuredContent: unknown }; + expect(result.content).toEqual([{ type: 'text', text: 'hi' }]); + expect(result.structuredContent).toEqual({ ok: true }); + }); }); }); diff --git a/scripts/fetch-schema-twins.ts b/scripts/fetch-schema-twins.ts new file mode 100644 index 0000000000..c6464e38b6 --- /dev/null +++ b/scripts/fetch-schema-twins.ts @@ -0,0 +1,73 @@ +/** + * Vendors the generated `schema.json` twins from the spec repository into + * `packages/core/test/corpus/schema-twins/` as RAW UPSTREAM BYTES. + * + * The twins are TEST-ONLY conformance oracles (never bundled, never runtime): + * `packages/core/test/wire/schemaTwinConformance.test.ts` compiles them into + * generated validators and locks the hand-written wire layer to them. Their + * authority rests on provenance, so they are vendored verbatim — no + * formatting of any kind (the directory is .prettierignore'd) — and each file + * is locked to the manifest's sha256/byte values at test time. Any rewrite + * (prettier, an editor, a manual touch-up) turns CI red. + * + * Refresh ATOMICALLY with the matching spec.types anchor (see + * packages/core/src/types/README.md lifecycle rule 4). + * + * Usage: + * pnpm fetch:schema-twins [sha] # default: the manifest's current source commit + * + * Sources are fetched from GitHub at the given commit, mirroring + * scripts/fetch-spec-types.ts; the manifest's provenance values (source + * commit, sha256, byte size) are recomputed from the fetched bytes. + */ + +import { createHash } from 'node:crypto'; +import { readFileSync, writeFileSync } from 'node:fs'; +import { dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const __filename = fileURLToPath(import.meta.url); +const PROJECT_ROOT = join(dirname(__filename), '..'); + +const SPEC_REPO = 'modelcontextprotocol/modelcontextprotocol'; +const TWINS_DIR = join(PROJECT_ROOT, 'packages', 'core', 'test', 'corpus', 'schema-twins'); +const MANIFEST_PATH = join(TWINS_DIR, 'manifest.json'); + +interface TwinManifest { + comment: string; + source: { repository: string; commit: string }; + files: Record; +} + +async function fetchRawBytes(sha: string, upstreamPath: string): Promise { + const url = `https://raw.githubusercontent.com/${SPEC_REPO}/${sha}/${upstreamPath}`; + const response = await fetch(url); + if (!response.ok) { + throw new Error(`Failed to fetch ${upstreamPath}: ${response.status} ${response.statusText}`); + } + return Buffer.from(await response.arrayBuffer()); +} + +async function main(): Promise { + const manifest = JSON.parse(readFileSync(MANIFEST_PATH, 'utf8')) as TwinManifest; + const sha = process.argv[2] ?? manifest.source.commit; + + for (const [revision, entry] of Object.entries(manifest.files)) { + console.log(`[${revision}] Fetching ${entry.upstreamPath} at ${sha}`); + const bytes = await fetchRawBytes(sha, entry.upstreamPath); + // Verbatim: the twin IS the upstream artifact, byte for byte. + writeFileSync(join(TWINS_DIR, `${revision}.schema.json`), bytes); + entry.sha256 = createHash('sha256').update(bytes).digest('hex'); + entry.bytes = bytes.byteLength; + console.log(`[${revision}] ${entry.bytes} bytes, sha256 ${entry.sha256}`); + } + + manifest.source = { repository: SPEC_REPO, commit: sha }; + writeFileSync(MANIFEST_PATH, `${JSON.stringify(manifest, null, 4)}\n`, 'utf8'); + console.log(`Updated ${MANIFEST_PATH}`); +} + +main().catch((error: unknown) => { + console.error('Error:', error instanceof Error ? error.message : String(error)); + process.exit(1); +}); diff --git a/scripts/fetch-spec-examples.ts b/scripts/fetch-spec-examples.ts new file mode 100644 index 0000000000..d20b73eb62 --- /dev/null +++ b/scripts/fetch-spec-examples.ts @@ -0,0 +1,150 @@ +/** + * Vendors the draft-revision (2026-07-28) example corpus from the spec + * repository into `packages/core/test/corpus/fixtures/2026-07-28/`. + * + * The spec repository ships canonical example instances for the draft schema + * (`schema/draft/examples//*.json`). The corpus harness + * (`packages/core/test/corpus/specCorpus.test.ts`) parses every vendored + * example through the SDK's wire schemas, so accept-side drift between the + * SDK and the specification turns CI red. + * + * Files are vendored verbatim, plus a `manifest.json` recording provenance + * (source commit) and the directory/file inventory so corpus drift is loud. + * + * Usage: + * pnpm fetch:spec-examples --spec-dir + * pnpm fetch:spec-examples [sha] # fetch from GitHub (default: latest main) + * + * With `--spec-dir`, examples are read from a local checkout of + * modelcontextprotocol/modelcontextprotocol (provenance is the checkout's + * HEAD commit). Without it, sources are fetched from GitHub at the given + * commit, mirroring scripts/fetch-spec-types.ts. + */ + +import { execFileSync } from 'node:child_process'; +import { mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs'; +import { dirname, join, resolve, sep } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const __filename = fileURLToPath(import.meta.url); +const PROJECT_ROOT = join(dirname(__filename), '..'); + +const SPEC_REPO = 'modelcontextprotocol/modelcontextprotocol'; +/** The upcoming protocol revision; its examples live in the spec repo's draft directory. */ +const DRAFT_REVISION = '2026-07-28'; +const EXAMPLES_PATH = 'schema/draft/examples'; +const OUTPUT_DIR = join(PROJECT_ROOT, 'packages', 'core', 'test', 'corpus', 'fixtures', DRAFT_REVISION); + +interface ExampleFile { + /** `/.json` relative to the examples root. */ + relPath: string; + content: string; +} + +async function fetchLatestSHA(): Promise { + const url = `https://api.github.com/repos/${SPEC_REPO}/commits?path=${EXAMPLES_PATH}&per_page=1`; + const response = await fetch(url); + if (!response.ok) throw new Error(`Failed to fetch commit info: ${response.status} ${response.statusText}`); + const commits = (await response.json()) as Array<{ sha: string }>; + if (!commits?.length) throw new Error('No commits found for the examples path'); + return commits[0].sha; +} + +async function listExamplesFromGitHub(sha: string): Promise { + const url = `https://api.github.com/repos/${SPEC_REPO}/git/trees/${sha}?recursive=1`; + const response = await fetch(url); + if (!response.ok) throw new Error(`Failed to fetch repo tree: ${response.status} ${response.statusText}`); + const tree = (await response.json()) as { truncated?: boolean; tree: Array<{ path: string; type: string }> }; + if (tree.truncated) throw new Error('GitHub tree listing truncated; cannot enumerate examples reliably'); + return tree.tree + .filter(entry => entry.type === 'blob' && entry.path.startsWith(`${EXAMPLES_PATH}/`) && entry.path.endsWith('.json')) + .map(entry => entry.path.slice(EXAMPLES_PATH.length + 1)); +} + +async function fetchExamplesFromGitHub(sha: string): Promise { + const relPaths = await listExamplesFromGitHub(sha); + const files: ExampleFile[] = []; + for (const relPath of relPaths) { + const url = `https://raw.githubusercontent.com/${SPEC_REPO}/${sha}/${EXAMPLES_PATH}/${relPath}`; + const response = await fetch(url); + if (!response.ok) throw new Error(`Failed to fetch ${relPath}: ${response.status} ${response.statusText}`); + files.push({ relPath, content: await response.text() }); + } + return files; +} + +function readExamplesFromDir(specDir: string): { files: ExampleFile[]; sha: string } { + const root = join(specDir, ...EXAMPLES_PATH.split('/')); + const files: ExampleFile[] = []; + for (const typeDir of readdirSync(root).sort()) { + const dirPath = join(root, typeDir); + if (!statSync(dirPath).isDirectory()) continue; + for (const file of readdirSync(dirPath).sort()) { + if (!file.endsWith('.json')) continue; + files.push({ relPath: `${typeDir}/${file}`, content: readFileSync(join(dirPath, file), 'utf8') }); + } + } + const sha = execFileSync('git', ['-C', specDir, 'rev-parse', 'HEAD'], { encoding: 'utf8' }).trim(); + return { files, sha }; +} + +function writeCorpus(files: ExampleFile[], sha: string): void { + if (files.length === 0) throw new Error('No example files found — refusing to write an empty corpus'); + + rmSync(OUTPUT_DIR, { recursive: true, force: true }); + mkdirSync(OUTPUT_DIR, { recursive: true }); + + const dirs: Record = {}; + for (const file of files.sort((a, b) => a.relPath.localeCompare(b.relPath))) { + // The path components come from outside this repo (a spec checkout or the + // GitHub trees API); reject anything that could escape the output directory. + const parts = file.relPath.split('/'); + if (parts.length !== 2 || parts.some(p => !p || p === '.' || p === '..' || p.includes('\\'))) { + throw new Error(`Unsafe or unexpected example path: ${file.relPath}`); + } + const [typeDir, fileName] = parts as [string, string]; + const destFile = resolve(OUTPUT_DIR, typeDir, fileName); + if (!destFile.startsWith(resolve(OUTPUT_DIR) + sep)) { + throw new Error(`Example path escapes the output directory: ${file.relPath}`); + } + mkdirSync(join(OUTPUT_DIR, typeDir), { recursive: true }); + // Validate now so a malformed upstream example fails the vendoring, not the harness. + JSON.parse(file.content); + writeFileSync(destFile, file.content); + (dirs[typeDir] ??= []).push(fileName); + } + + const manifest = { + revision: DRAFT_REVISION, + source: { repo: SPEC_REPO, path: EXAMPLES_PATH, commit: sha }, + regenerate: 'pnpm fetch:spec-examples --spec-dir # or [sha] to fetch from GitHub', + directoryCount: Object.keys(dirs).length, + fileCount: files.length, + directories: dirs + }; + writeFileSync(join(OUTPUT_DIR, 'manifest.json'), `${JSON.stringify(manifest, null, 4)}\n`); + + console.log(`Vendored ${files.length} example files across ${Object.keys(dirs).length} directories (source ${sha.slice(0, 8)})`); +} + +async function main(): Promise { + const args = process.argv.slice(2); + const specDirIndex = args.indexOf('--spec-dir'); + + if (specDirIndex !== -1) { + const specDir = args[specDirIndex + 1]; + if (!specDir) throw new Error('--spec-dir requires a path argument'); + const { files, sha } = readExamplesFromDir(specDir); + writeCorpus(files, sha); + return; + } + + const sha = args[0] ?? (await fetchLatestSHA()); + const files = await fetchExamplesFromGitHub(sha); + writeCorpus(files, sha); +} + +main().catch((error: unknown) => { + console.error(error); + process.exit(1); +}); diff --git a/scripts/fetch-spec-types.ts b/scripts/fetch-spec-types.ts index b0db8d486f..e1b1ee0eab 100644 --- a/scripts/fetch-spec-types.ts +++ b/scripts/fetch-spec-types.ts @@ -27,6 +27,23 @@ const UPSTREAM_SCHEMA_DIRS: Record = { '2026-07-28': 'draft' }; +/** + * Generation pin per released revision. Released revisions are frozen: without + * an explicit SHA argument, their types are regenerated from the pinned spec + * commit below — never from the latest upstream commit — so a released anchor + * can only change through a deliberate, reviewed repin. Moving a pin (or + * freezing a newly released revision) must land in the same commit that + * retargets `.github/workflows/update-spec-types.yml`. + * + * Draft-tracking revisions have no entry and float to the latest upstream + * commit via the nightly workflow's refresh PRs. + * + * See `packages/core/src/types/README.md` for the full lifecycle policy. + */ +const RELEASED_REVISION_PINS: Partial> = { + '2025-11-25': '0168c57fc74aba6e6dcf8f0b7191db3caaa5ad65' +}; + interface GitHubCommit { sha: string; } @@ -59,10 +76,14 @@ async function fetchSpecTypes(version: SpecVersion, sha: string): Promise { + const pinnedSHA = RELEASED_REVISION_PINS[version]; let sha: string; if (providedSHA) { console.log(`[${version}] Using provided SHA: ${providedSHA}`); sha = providedSHA; + } else if (pinnedSHA) { + console.log(`[${version}] Using pinned SHA for released revision: ${pinnedSHA}`); + sha = pinnedSHA; } else { console.log(`[${version}] Fetching latest commit SHA...`); sha = await fetchLatestSHA(version); diff --git a/test/e2e/requirements.ts b/test/e2e/requirements.ts index ea471a21fc..750b8e537b 100644 --- a/test/e2e/requirements.ts +++ b/test/e2e/requirements.ts @@ -154,6 +154,13 @@ export const REQUIREMENTS: Record = { behavior: 'The receiver silently ignores a cancellation notification referencing an unknown or already-completed request id; no error response is sent and no exception is raised.' }, + 'typescript:client:raw-result-type-first': { + source: 'sdk', + behavior: + 'A raw input_required result body through the full client path surfaces the discriminated kind as a typed local error (UNSUPPORTED_RESULT_TYPE with data.resultType) — never an empty-content success, on any spec-version axis.', + transports: ['inMemory', 'streamableHttp'], + note: 'The client funnel inspects the raw resultType before schema validation, closing the masking hazard where the tools/call result schema would default content to [] and report a hollow success. Raw relay servers stand in for a 2026-era peer; the streamableHttp leg uses a hand handler (custom fetch), so the cells exercise both an in-process and an HTTP response path.' + }, 'typescript:protocol:error:connection-closed': { source: 'sdk', behavior: 'Closing the transport invokes onclose and rejects all in-flight requests with ErrorCode.ConnectionClosed.', diff --git a/test/e2e/scenarios/raw-result-type.test.ts b/test/e2e/scenarios/raw-result-type.test.ts new file mode 100644 index 0000000000..35956810fe --- /dev/null +++ b/test/e2e/scenarios/raw-result-type.test.ts @@ -0,0 +1,161 @@ +/** + * Raw-first result discrimination through the full client path — ERA-SCOPED + * (Q1 increment 2: V-1 lives in the era codec's decodeResult, and the + * postures are ruled per era by Q1-SD3). + * + * A raw relay server (no SDK Server involved) answers tools/call with hand + * built bodies. The negotiated protocol version selects the wire era: + * + * - Negotiated 2026-07-28: `resultType` is the REQUIRED discriminator. An + * `input_required` body surfaces the discriminated kind as a typed local + * error (the multi-round-trip driver consumes it when it lands); an + * ABSENT `resultType` is a spec violation surfaced as a typed error + * naming it. + * - Negotiated legacy (2025 era): `resultType` is FOREIGN vocabulary — + * strip-on-lift (Q1-SD3 ii; a deliberate, ledgered change from the + * pre-split era-blind rejection — changeset: codec-split-wire-break). The + * stripped body then fails the (default-free) result schema loudly + * because it has no content. + * + * Either way the V-1 invariant holds: never an empty-content success. + */ +import { Client, SdkError, SdkErrorCode, StreamableHTTPClientTransport } from '@modelcontextprotocol/client'; +import type { JSONRPCRequest } from '@modelcontextprotocol/server'; +import { InMemoryTransport, LATEST_PROTOCOL_VERSION } from '@modelcontextprotocol/server'; +import { expect } from 'vitest'; + +import { verifies } from '../helpers/verifies.js'; +import type { TestArgs } from '../types.js'; + +const INPUT_REQUIRED_BODY = { + resultType: 'input_required', + inputRequests: { + 'elicit-1': { + method: 'elicitation/create', + params: { mode: 'form', message: 'What is your name?', requestedSchema: { type: 'object', properties: {} } } + } + }, + requestState: 'opaque-state' +}; + +/** A complete-looking body that omits the (2026-required) resultType. */ +const ABSENT_RESULT_TYPE_BODY = { content: [{ type: 'text', text: 'looks complete' }] }; + +function initializeResult(requestedVersion: string) { + return { + protocolVersion: requestedVersion, + capabilities: { tools: {} }, + serverInfo: { name: 'raw-input-required-server', version: '0' } + }; +} + +function makeResponder(toolCallBody: unknown) { + return function respondTo(request: JSONRPCRequest): unknown { + if (request.method === 'initialize') { + const requested = (request.params as { protocolVersion?: string } | undefined)?.protocolVersion ?? LATEST_PROTOCOL_VERSION; + return initializeResult(requested); + } + if (request.method === 'tools/call') return toolCallBody; + return {}; + }; +} + +async function connectInMemory(client: Client, toolCallBody: unknown): Promise { + const respondTo = makeResponder(toolCallBody); + const [clientTx, serverTx] = InMemoryTransport.createLinkedPair(); + serverTx.onmessage = message => { + const request = message as JSONRPCRequest; + if (request.id === undefined) return; // notifications need no answer + void serverTx.send({ jsonrpc: '2.0', id: request.id, result: respondTo(request) } as Parameters[0]); + }; + await serverTx.start(); + await client.connect(clientTx); +} + +async function connectStreamableHttp(client: Client, toolCallBody: unknown): Promise { + const respondTo = makeResponder(toolCallBody); + // A hand HTTP handler (no SDK server): JSON responses, 202 for notifications. + const fetchHandler = async (input: URL | string, init?: RequestInit): Promise => { + const request = new Request(input, init); + if (request.method !== 'POST') return new Response(null, { status: 405 }); + const body = (await request.json()) as JSONRPCRequest | JSONRPCRequest[]; + const message = Array.isArray(body) ? body[0] : body; + if (message?.id === undefined) return new Response(null, { status: 202 }); + return Response.json({ jsonrpc: '2.0', id: message.id, result: respondTo(message) }); + }; + await client.connect(new StreamableHTTPClientTransport(new URL('http://in-process/mcp'), { fetch: fetchHandler })); +} + +async function callToolOutcome(client: Client): Promise<{ resolved: unknown } | { rejected: unknown }> { + return client.callTool({ name: 'anything', arguments: {} }).then( + result => ({ resolved: result as unknown }), + error => ({ rejected: error as unknown }) + ); +} + +verifies('typescript:client:raw-result-type-first', async ({ transport }: TestArgs) => { + // ---- Legacy negotiation (the relay echoes the client's default offer, + // so this connection negotiates a legacy version → 2025 era). ---- + { + const client = new Client({ name: 'raw-result-type-client', version: '0' }); + await (transport === 'inMemory' + ? connectInMemory(client, INPUT_REQUIRED_BODY) + : connectStreamableHttp(client, INPUT_REQUIRED_BODY)); + + try { + const outcome = await callToolOutcome(client); + // Strip-on-lift (Q1-SD3 ii, ledgered): the foreign resultType is + // dropped; the body has no content, so validation fails LOUDLY. + // Never an empty-content success. + expect('resolved' in outcome, `must not resolve: ${JSON.stringify(outcome)}`).toBe(false); + const rejection = (outcome as { rejected: unknown }).rejected; + expect(rejection).toBeInstanceOf(SdkError); + expect((rejection as SdkError).code).toBe(SdkErrorCode.InvalidResult); + } finally { + await client.close(); + } + } + + // ---- Modern negotiation: the client opts into the draft revision, the + // relay echoes it back → 2026 era → V-1 discrimination in the codec. ---- + { + const client = new Client({ name: 'raw-result-type-client', version: '0' }, { supportedProtocolVersions: ['2026-07-28'] }); + await (transport === 'inMemory' + ? connectInMemory(client, INPUT_REQUIRED_BODY) + : connectStreamableHttp(client, INPUT_REQUIRED_BODY)); + + try { + const outcome = await callToolOutcome(client); + expect('resolved' in outcome, `must not resolve: ${JSON.stringify(outcome)}`).toBe(false); + const rejection = (outcome as { rejected: unknown }).rejected; + expect(rejection).toBeInstanceOf(SdkError); + const typed = rejection as SdkError; + expect(typed.code).toBe(SdkErrorCode.UnsupportedResultType); + expect(typed.data).toMatchObject({ resultType: 'input_required', method: 'tools/call' }); + } finally { + await client.close(); + } + } + + // ---- Modern negotiation, absent resultType: the spec violation is + // surfaced as a typed error naming it (Q1-SD3 i — the absent⇒complete + // bridge applies only to earlier-revision servers). ---- + { + const client = new Client({ name: 'raw-result-type-client', version: '0' }, { supportedProtocolVersions: ['2026-07-28'] }); + await (transport === 'inMemory' + ? connectInMemory(client, ABSENT_RESULT_TYPE_BODY) + : connectStreamableHttp(client, ABSENT_RESULT_TYPE_BODY)); + + try { + const outcome = await callToolOutcome(client); + expect('resolved' in outcome, `must not resolve: ${JSON.stringify(outcome)}`).toBe(false); + const rejection = (outcome as { rejected: unknown }).rejected; + expect(rejection).toBeInstanceOf(SdkError); + const typed = rejection as SdkError; + expect(typed.code).toBe(SdkErrorCode.InvalidResult); + expect(String(typed.message)).toContain('missing required resultType'); + } finally { + await client.close(); + } + } +}); diff --git a/test/integration/test/client/client.test.ts b/test/integration/test/client/client.test.ts index a7613b24e4..89ea643edb 100644 --- a/test/integration/test/client/client.test.ts +++ b/test/integration/test/client/client.test.ts @@ -171,6 +171,42 @@ test('should restore negotiated protocol version on transport when reconnecting expect(reconnectSetProtocolVersion).toHaveBeenCalledWith(LATEST_PROTOCOL_VERSION); }); +/*** + * Test: The negotiated protocol version (and with it the wire era) is connection state — it must + * not survive into a fresh connect. A client whose previous connection negotiated the modern + * revision (2026-07-28) must still be able to run a FRESH initialize handshake: `initialize` is + * legacy-era vocabulary by definition (it is physically absent from the modern registry), so a + * negotiated version left over from the dead connection would otherwise kill the handshake + * locally before it reaches the transport. + */ +test('should run a fresh initialize handshake after close() when the previous connection negotiated the modern era', async () => { + const MODERN_REVISION = '2026-07-28'; + const supportedProtocolVersions = [MODERN_REVISION, ...SUPPORTED_PROTOCOL_VERSIONS]; + + const connectModern = async (client: Client) => { + const server = new Server({ name: 'modern server', version: '1.0' }, { capabilities: {}, supportedProtocolVersions }); + const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); + await server.connect(serverTransport); + await client.connect(clientTransport); + }; + + const client = new Client({ name: 'test client', version: '1.0' }, { supportedProtocolVersions }); + + // First connection negotiates the modern revision: the instance now speaks the modern wire era. + await connectModern(client); + expect(client.getNegotiatedProtocolVersion()).toBe(MODERN_REVISION); + + await client.close(); + + // Fresh connect (new transport, no sessionId): the stale negotiated version is cleared, the + // handshake rides the pre-negotiation bootstrap pin (legacy era), and the connection + // can re-negotiate the modern revision. + await connectModern(client); + expect(client.getNegotiatedProtocolVersion()).toBe(MODERN_REVISION); + + await client.close(); +}); + /*** * Test: Reject Unsupported Protocol Version */ @@ -1769,6 +1805,9 @@ describe('outputSchema validation', () => { server.setRequestHandler('tools/call', async request => { if (request.params.name === 'test-tool') { return { + // content is spec-required (the wire default([]) was removed + // - ledgered; changeset codec-split-wire-break) + content: [], structuredContent: { result: 'success', count: 42 } }; } @@ -1844,6 +1883,7 @@ describe('outputSchema validation', () => { if (request.params.name === 'test-tool') { // Return invalid structured content (count is string instead of number) return { + content: [], structuredContent: { result: 'success', count: 'not a number' } }; } @@ -2071,6 +2111,7 @@ describe('outputSchema validation', () => { server.setRequestHandler('tools/call', async request => { if (request.params.name === 'complex-tool') { return { + content: [], structuredContent: { name: 'John Doe', age: 30, @@ -2156,6 +2197,7 @@ describe('outputSchema validation', () => { if (request.params.name === 'strict-tool') { // Return structured content with extra property return { + content: [], structuredContent: { name: 'John', extraField: 'not allowed' diff --git a/test/integration/test/taskResumability.test.ts b/test/integration/test/transportResumability.test.ts similarity index 100% rename from test/integration/test/taskResumability.test.ts rename to test/integration/test/transportResumability.test.ts