From 468e952a07f4fdb7d8ce309409c71b8a8fbc4567 Mon Sep 17 00:00:00 2001 From: squarepots <46488165+squarepots@users.noreply.github.com> Date: Thu, 24 Sep 2026 23:41:18 +0800 Subject: [PATCH 1/6] fix(spec): freeze 2026-07-28 release references --- .github/workflows/update-spec-types.yml | 95 ------------------- packages/core-internal/src/types/README.md | 10 +- .../src/types/spec.types.2026-07-28.ts | 29 ++++-- .../src/wire/rev2026-07-28/buildSchemas.ts | 6 +- .../src/wire/rev2026-07-28/schemas.ts | 1 + .../listen-closed-response.json | 10 ++ .../corpus/fixtures/2026-07-28/manifest.json | 11 ++- .../schema-twins/2026-07-28.schema.json | 25 ++++- .../test/corpus/schema-twins/manifest.json | 15 +-- .../test/corpus/specCorpus.test.ts | 16 ++-- .../test/spec.types.2026-07-28.test.ts | 14 ++- .../test/wire/schemaTwinConformance.test.ts | 5 +- packages/server/src/server/listenRouter.ts | 4 +- packages/server/src/server/server.ts | 2 +- .../server/createMcpHandlerListen.test.ts | 2 +- .../test/server/serveStdioListen.test.ts | 2 +- scripts/fetch-schema-twins.ts | 47 ++++++--- scripts/fetch-spec-examples.ts | 31 +++--- scripts/fetch-spec-types.ts | 23 ++--- 19 files changed, 160 insertions(+), 188 deletions(-) delete mode 100644 .github/workflows/update-spec-types.yml create mode 100644 packages/core-internal/test/corpus/fixtures/2026-07-28/SubscriptionsListenResultResponse/listen-closed-response.json diff --git a/.github/workflows/update-spec-types.yml b/.github/workflows/update-spec-types.yml deleted file mode 100644 index c62aca8907..0000000000 --- a/.github/workflows/update-spec-types.yml +++ /dev/null @@ -1,95 +0,0 @@ -# Nightly refresh of the draft-tracking spec anchor (2026-07-28). -# -# Anchor lifecycle (see packages/core-internal/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: - schedule: - # Run nightly at 4 AM UTC - - cron: '0 4 * * *' - workflow_dispatch: - -permissions: - contents: write - pull-requests: write - -jobs: - update-spec-types: - runs-on: ubuntu-latest - steps: - - name: Checkout repository - uses: actions/checkout@v7 - - - name: Install pnpm - uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0 - id: pnpm-install - with: - run_install: false - - - name: Setup Node.js - uses: actions/setup-node@v6 - with: - node-version: 24 - cache: pnpm - cache-dependency-path: pnpm-lock.yaml - - - name: Install dependencies - run: pnpm install - - - name: Fetch latest spec types - run: pnpm run fetch:spec-types 2026-07-28 - - - name: Check for changes - id: check_changes - run: | - if git diff --quiet packages/core-internal/src/types/spec.types.2026-07-28.ts; then - echo "has_changes=false" >> $GITHUB_OUTPUT - else - echo "has_changes=true" >> $GITHUB_OUTPUT - LATEST_SHA=$(grep "Last updated from commit:" packages/core-internal/src/types/spec.types.2026-07-28.ts | cut -d: -f2 | tr -d ' ') - echo "sha=$LATEST_SHA" >> $GITHUB_OUTPUT - fi - - - name: Create Pull Request - if: steps.check_changes.outputs.has_changes == 'true' - env: - GH_TOKEN: ${{ github.token }} - # Skip lefthook pre-push (typecheck/lint/build); spec drift that breaks - # typecheck should still open a PR so it can be fixed there. - LEFTHOOK: 0 - run: | - git config user.name "github-actions[bot]" - git config user.email "github-actions[bot]@users.noreply.github.com" - - git checkout -B update-spec-types - git add packages/core-internal/src/types/spec.types.2026-07-28.ts - git commit -m "chore: update spec.types.2026-07-28.ts from upstream" - git push -f --no-verify origin update-spec-types - - # Create PR if it doesn't exist, or update if it does - PR_BODY="This PR updates \`packages/core-internal/src/types/spec.types.2026-07-28.ts\` from the Model Context Protocol specification. - - Source file: https://github.com/modelcontextprotocol/modelcontextprotocol/blob/${{ steps.check_changes.outputs.sha }}/schema/draft/schema.ts - - This is an automated update triggered by the nightly cron job." - - # `gh pr view ` matches closed PRs too, so check for an *open* PR explicitly. - EXISTING_PR=$(gh pr list --head update-spec-types --state open --json number --jq '.[0].number // empty') - if [ -n "$EXISTING_PR" ]; then - echo "PR #$EXISTING_PR already exists, updating description..." - gh pr edit "$EXISTING_PR" --body "$PR_BODY" - else - gh pr create \ - --title "chore: update spec.types.2026-07-28.ts from upstream" \ - --body "$PR_BODY" \ - --base main \ - --head update-spec-types - fi diff --git a/packages/core-internal/src/types/README.md b/packages/core-internal/src/types/README.md index fda450e8e9..374032f85d 100644 --- a/packages/core-internal/src/types/README.md +++ b/packages/core-internal/src/types/README.md @@ -8,11 +8,9 @@ They are reference-only test oracles: the comparison suites in `packages/core-in ## 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. + 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 atomically with every checked-in oracle derived from that revision. -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. +2. **Draft anchors float only while they have an explicit consumer.** An unreleased revision may track the spec repository's `schema/draft/schema.ts`, but refreshes must go through reviewed PRs and update all derived oracles atomically. No currently supported revision uses this mode. 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. @@ -20,7 +18,7 @@ They are reference-only test oracles: the comparison suites in `packages/core-in 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-internal/test/corpus/schema-twins/` (`.schema.json` + `manifest.json` recording the source commit and content hashes). They are TEST-ONLY oracles consumed by the + **This clause is OPERATIVE.** The vendored twins are the per-revision `schema.json` copies under `packages/core-internal/test/corpus/schema-twins/` (`.schema.json` + `manifest.json` recording each revision's 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 + `schema//schema.json` from that revision's pinned source 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-internal/src/types/spec.types.2026-07-28.ts b/packages/core-internal/src/types/spec.types.2026-07-28.ts index f4430b850f..36ce3b043d 100644 --- a/packages/core-internal/src/types/spec.types.2026-07-28.ts +++ b/packages/core-internal/src/types/spec.types.2026-07-28.ts @@ -2,8 +2,8 @@ * This file is automatically generated from the Model Context Protocol specification. * * Source: https://github.com/modelcontextprotocol/modelcontextprotocol - * Pulled from: https://raw.githubusercontent.com/modelcontextprotocol/modelcontextprotocol/main/schema/draft/schema.ts - * Last updated from commit: 71e306956a4959c9655e5036be215d41986596e6 + * Pulled from: https://raw.githubusercontent.com/modelcontextprotocol/modelcontextprotocol/main/schema/2026-07-28/schema.ts + * Last updated from commit: 271ecc9accafdd9b83a3c869fa67c22953b2af80 * * 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 @@ -55,7 +55,7 @@ export const JSONRPC_VERSION = '2.0'; * - Unless empty, MUST start and end with an alphanumeric character (`[a-z0-9A-Z]`). * - Interior characters may be alphanumeric, hyphens (`-`), underscores (`_`), or dots (`.`). * - * @see [General fields: `_meta`](/specification/draft/basic/index#meta) for more details. + * @see [General fields: `_meta`](/specification/2026-07-28/basic/index#meta) for more details. * @category Common Types */ export type MetaObject = Record; @@ -64,7 +64,7 @@ export type MetaObject = Record; * Extends {@link MetaObject} with additional request-specific fields. All key naming rules from `MetaObject` apply. * * @see {@link MetaObject} for key naming rules and reserved prefixes. - * @see [General fields: `_meta`](/specification/draft/basic/index#meta) for more details. + * @see [General fields: `_meta`](/specification/2026-07-28/basic/index#meta) for more details. * @category Common Types */ export interface RequestMetaObject extends MetaObject { @@ -121,7 +121,7 @@ export interface RequestMetaObject extends MetaObject { * Extends {@link MetaObject} with additional notification-specific fields. All key naming rules from `MetaObject` apply. * * @see {@link MetaObject} for key naming rules and reserved prefixes. - * @see [General fields: `_meta`](/specification/draft/basic/index#meta) for more details. + * @see [General fields: `_meta`](/specification/2026-07-28/basic/index#meta) for more details. * @category Common Types */ export interface NotificationMetaObject extends MetaObject { @@ -144,7 +144,7 @@ export interface NotificationMetaObject extends MetaObject { * Extends {@link MetaObject} with additional result-specific fields. All key naming rules from `MetaObject` apply. * * @see {@link MetaObject} for key naming rules and reserved prefixes. - * @see [General fields: `_meta`](/specification/draft/basic/index#meta) for more details. + * @see [General fields: `_meta`](/specification/2026-07-28/basic/index#meta) for more details. * @category Common Types */ export interface ResultMetaObject extends MetaObject { @@ -1317,7 +1317,7 @@ export interface SubscriptionsListenRequest extends JSONRPCRequest { * @see {@link MetaObject} for key naming rules and reserved prefixes. * @category `subscriptions/listen` */ -export interface SubscriptionsListenResultMeta extends ResultMetaObject { +export interface SubscriptionsListenResultMetaObject extends ResultMetaObject { /** * Identifies the subscription stream this response closes, so the client can * correlate it with the originating subscription — mirroring the same key on @@ -1341,7 +1341,20 @@ export interface SubscriptionsListenResultMeta extends ResultMetaObject { * @category `subscriptions/listen` */ export interface SubscriptionsListenResult extends Result { - _meta: SubscriptionsListenResultMeta; + _meta: SubscriptionsListenResultMetaObject; +} + +/** + * A successful response from the server for a {@link SubscriptionsListenRequest | subscriptions/listen} + * request, sent when the server tears the subscription down gracefully. + * + * @example Subscription closed gracefully response + * {@includeCode ./examples/SubscriptionsListenResultResponse/listen-closed-response.json} + * + * @category `subscriptions/listen` + */ +export interface SubscriptionsListenResultResponse extends JSONRPCResultResponse { + result: SubscriptionsListenResult; } /** diff --git a/packages/core-internal/src/wire/rev2026-07-28/buildSchemas.ts b/packages/core-internal/src/wire/rev2026-07-28/buildSchemas.ts index 6831062fd3..f286b3cffa 100644 --- a/packages/core-internal/src/wire/rev2026-07-28/buildSchemas.ts +++ b/packages/core-internal/src/wire/rev2026-07-28/buildSchemas.ts @@ -1070,7 +1070,7 @@ function build() { const SubscriptionsListenRequestSchema = wireRequest('subscriptions/listen', subscriptionsListenParamsShape); /** - * Anchor SubscriptionsListenResultMeta — required subscriptionId stamp on + * Anchor SubscriptionsListenResultMetaObject — required subscriptionId stamp on * the graceful-close result. Extends `ResultMetaObject` since spec PR * #3002 (composed, so the serverInfo key and its leniency stay single-sourced). */ @@ -1276,6 +1276,7 @@ function build() { const ReadResourceResultResponseSchema = wireResultResponse(z.union([ReadResourceResultSchema, InputRequiredResultSchema])); const CompleteResultResponseSchema = wireResultResponse(CompleteResultSchema); const DiscoverResultResponseSchema = wireResultResponse(DiscoverResultSchema); + const SubscriptionsListenResultResponseSchema = wireResultResponse(SubscriptionsListenResultSchema); return { JSONValueSchema, @@ -1411,7 +1412,8 @@ function build() { ListResourceTemplatesResultResponseSchema, ReadResourceResultResponseSchema, CompleteResultResponseSchema, - DiscoverResultResponseSchema + DiscoverResultResponseSchema, + SubscriptionsListenResultResponseSchema }; } diff --git a/packages/core-internal/src/wire/rev2026-07-28/schemas.ts b/packages/core-internal/src/wire/rev2026-07-28/schemas.ts index b342ded914..d1ff969736 100644 --- a/packages/core-internal/src/wire/rev2026-07-28/schemas.ts +++ b/packages/core-internal/src/wire/rev2026-07-28/schemas.ts @@ -156,3 +156,4 @@ export const ListResourceTemplatesResultResponseSchema = s.ListResourceTemplates export const ReadResourceResultResponseSchema = s.ReadResourceResultResponseSchema; export const CompleteResultResponseSchema = s.CompleteResultResponseSchema; export const DiscoverResultResponseSchema = s.DiscoverResultResponseSchema; +export const SubscriptionsListenResultResponseSchema = s.SubscriptionsListenResultResponseSchema; diff --git a/packages/core-internal/test/corpus/fixtures/2026-07-28/SubscriptionsListenResultResponse/listen-closed-response.json b/packages/core-internal/test/corpus/fixtures/2026-07-28/SubscriptionsListenResultResponse/listen-closed-response.json new file mode 100644 index 0000000000..9441979a42 --- /dev/null +++ b/packages/core-internal/test/corpus/fixtures/2026-07-28/SubscriptionsListenResultResponse/listen-closed-response.json @@ -0,0 +1,10 @@ +{ + "jsonrpc": "2.0", + "id": "listen-1", + "result": { + "resultType": "complete", + "_meta": { + "io.modelcontextprotocol/subscriptionId": "listen-1" + } + } +} diff --git a/packages/core-internal/test/corpus/fixtures/2026-07-28/manifest.json b/packages/core-internal/test/corpus/fixtures/2026-07-28/manifest.json index eeeae659cd..b47980fe67 100644 --- a/packages/core-internal/test/corpus/fixtures/2026-07-28/manifest.json +++ b/packages/core-internal/test/corpus/fixtures/2026-07-28/manifest.json @@ -2,12 +2,12 @@ "revision": "2026-07-28", "source": { "repo": "modelcontextprotocol/modelcontextprotocol", - "path": "schema/draft/examples", - "commit": "71e306956a4959c9655e5036be215d41986596e6" + "path": "schema/2026-07-28/examples", + "commit": "271ecc9accafdd9b83a3c869fa67c22953b2af80" }, "regenerate": "pnpm fetch:spec-examples --spec-dir # or [sha] to fetch from GitHub", - "directoryCount": 87, - "fileCount": 128, + "directoryCount": 88, + "fileCount": 129, "directories": { "AudioContent": [ "audio-wav-content.json" @@ -273,6 +273,9 @@ "SubscriptionsListenResult": [ "listen-closed.json" ], + "SubscriptionsListenResultResponse": [ + "listen-closed-response.json" + ], "TextContent": [ "text-content.json" ], diff --git a/packages/core-internal/test/corpus/schema-twins/2026-07-28.schema.json b/packages/core-internal/test/corpus/schema-twins/2026-07-28.schema.json index cc44564e33..213c58f6d9 100644 --- a/packages/core-internal/test/corpus/schema-twins/2026-07-28.schema.json +++ b/packages/core-internal/test/corpus/schema-twins/2026-07-28.schema.json @@ -3400,7 +3400,7 @@ "description": "The response to a {@link SubscriptionsListenRequestsubscriptions/listen}\nrequest, signalling that the subscription has ended gracefully (for example,\nduring server shutdown). Because the listen stream is long-lived, this result\nis sent only when the server tears the subscription down; an abrupt transport\nclose carries no response. The result body is otherwise empty.", "properties": { "_meta": { - "$ref": "#/$defs/SubscriptionsListenResultMeta" + "$ref": "#/$defs/SubscriptionsListenResultMetaObject" }, "resultType": { "description": "Indicates the type of the result, which allows the client to determine\nhow to parse the result object.\n\nServers implementing this protocol version MUST include this field.\nFor backward compatibility, when a client receives a result from a\nserver implementing an earlier protocol version (which does not include\n`resultType`), the client MUST treat the absent field as `\"complete\"`.", @@ -3413,7 +3413,7 @@ ], "type": "object" }, - "SubscriptionsListenResultMeta": { + "SubscriptionsListenResultMetaObject": { "description": "Extends {@link ResultMetaObject} with the subscription-stream identifier carried by a\n{@link SubscriptionsListenResult}. All key naming rules from `MetaObject` apply.", "properties": { "io.modelcontextprotocol/serverInfo": { @@ -3430,6 +3430,27 @@ ], "type": "object" }, + "SubscriptionsListenResultResponse": { + "description": "A successful response from the server for a {@link SubscriptionsListenRequestsubscriptions/listen}\nrequest, sent when the server tears the subscription down gracefully.", + "properties": { + "id": { + "$ref": "#/$defs/RequestId" + }, + "jsonrpc": { + "const": "2.0", + "type": "string" + }, + "result": { + "$ref": "#/$defs/SubscriptionsListenResult" + } + }, + "required": [ + "id", + "jsonrpc", + "result" + ], + "type": "object" + }, "TextContent": { "description": "Text provided to or from an LLM.", "properties": { diff --git a/packages/core-internal/test/corpus/schema-twins/manifest.json b/packages/core-internal/test/corpus/schema-twins/manifest.json index 9047083294..10be62d587 100644 --- a/packages/core-internal/test/corpus/schema-twins/manifest.json +++ b/packages/core-internal/test/corpus/schema-twins/manifest.json @@ -1,19 +1,20 @@ { - "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-internal/src/types/README.md lifecycle rule 4).", + "comment": "Vendored schema.json twins (TEST-ONLY conformance oracles; never bundled, never runtime). RAW upstream bytes - never reformat: each file is locked to its per-revision sourceCommit/sha256/bytes by schemaTwinConformance. Refresh via `pnpm fetch:schema-twins [revision] [sha]` (see packages/core-internal/src/types/README.md lifecycle rule 4).", "source": { - "repository": "modelcontextprotocol/modelcontextprotocol", - "commit": "71e306956a4959c9655e5036be215d41986596e6" + "repository": "modelcontextprotocol/modelcontextprotocol" }, "files": { "2026-07-28": { - "sha256": "9281c4890630e2d1e61792fa23b4084c4ea360cd58519610cd050545ab7b8708", - "bytes": 180695, - "upstreamPath": "schema/draft/schema.json" + "sha256": "ef70b61f99b6d2e5e3b46863822eab08dff6a45bedc7a08914e0e5b133f40203", + "bytes": 181474, + "upstreamPath": "schema/2026-07-28/schema.json", + "sourceCommit": "271ecc9accafdd9b83a3c869fa67c22953b2af80" }, "2025-11-25": { "sha256": "7b2d96fd95efd2216aa953606b83f5a740ddeaa5ebd3a5d27b45a8296545a118", "bytes": 174326, - "upstreamPath": "schema/2025-11-25/schema.json" + "upstreamPath": "schema/2025-11-25/schema.json", + "sourceCommit": "0168c57fc74aba6e6dcf8f0b7191db3caaa5ad65" } } } diff --git a/packages/core-internal/test/corpus/specCorpus.test.ts b/packages/core-internal/test/corpus/specCorpus.test.ts index 64a5bd1fb2..5c8a0393fa 100644 --- a/packages/core-internal/test/corpus/specCorpus.test.ts +++ b/packages/core-internal/test/corpus/specCorpus.test.ts @@ -3,8 +3,8 @@ * * Two corpora, one harness: * - * - `fixtures/2026-07-28/` is VENDORED from the spec repository's draft - * example set (`schema/draft/examples/`), regenerated only via + * - `fixtures/2026-07-28/` is VENDORED from the released spec repository + * example set (`schema/2026-07-28/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. @@ -59,7 +59,7 @@ const ERROR_OBJECT_DIRS = new Set([ ]); /** - * Draft (2026-07-28) vocabulary the SDK does not model yet, at directory + * 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. @@ -72,7 +72,7 @@ const PENDING_2026: Record = { /** * 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 + * revision-specific 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 = { @@ -172,12 +172,14 @@ 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 }; + source: { path: string; commit: string }; directoryCount: number; fileCount: number; directories: Record; }; expect(manifest.revision).toBe('2026-07-28'); + expect(manifest.source.path).toBe('schema/2026-07-28/examples'); + expect(manifest.source.commit).toBe('271ecc9accafdd9b83a3c869fa67c22953b2af80'); const dirs = listTypeDirs('2026-07-28'); expect(dirs).toEqual(Object.keys(manifest.directories).sort()); @@ -186,8 +188,8 @@ describe('corpus inventory pins', () => { // 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(87); - expect(manifest.fileCount).toBe(128); + expect(manifest.directoryCount).toBe(88); + expect(manifest.fileCount).toBe(129); }); test('the frozen 2025-11-25 corpus keeps its inventory', () => { diff --git a/packages/core-internal/test/spec.types.2026-07-28.test.ts b/packages/core-internal/test/spec.types.2026-07-28.test.ts index 4bdc3d11fc..bf6ed2613f 100644 --- a/packages/core-internal/test/spec.types.2026-07-28.test.ts +++ b/packages/core-internal/test/spec.types.2026-07-28.test.ts @@ -91,6 +91,7 @@ type WSubscriptionsAcknowledgedNotification = z4.infer; type WSubscriptionsListenResultMeta = z4.infer; +type WSubscriptionsListenResultResponse = z4.infer; // The anchor's ClientRequest union, composed from the era module's wire requests. type WClientRequest = | WCompleteRequest @@ -804,7 +805,14 @@ const wireParityChecks = { sdk = spec; spec = sdk; }, - SubscriptionsListenResultMeta: (sdk: WSubscriptionsListenResultMeta, spec: SpecTypes.SubscriptionsListenResultMeta) => { + SubscriptionsListenResultMetaObject: (sdk: WSubscriptionsListenResultMeta, spec: SpecTypes.SubscriptionsListenResultMetaObject) => { + sdk = spec; + spec = sdk; + }, + SubscriptionsListenResultResponse: ( + sdk: WSubscriptionsListenResultResponse, + spec: SpecTypes.SubscriptionsListenResultResponse + ) => { sdk = spec; spec = sdk; }, @@ -874,8 +882,10 @@ describe('Spec Types (2026-07-28)', () => { expect(specTypes).toContain('InputRequiredResult'); expect(specTypes).toContain('SubscriptionsListenRequest'); expect(specTypes).toContain('SubscriptionsListenResult'); + expect(specTypes).toContain('SubscriptionsListenResultMetaObject'); + expect(specTypes).toContain('SubscriptionsListenResultResponse'); expect(specTypes).toContain('ResultMetaObject'); - expect(specTypes).toHaveLength(154); + expect(specTypes).toHaveLength(155); }); it('should only allowlist types that exist in the 2026-07-28 schema', () => { diff --git a/packages/core-internal/test/wire/schemaTwinConformance.test.ts b/packages/core-internal/test/wire/schemaTwinConformance.test.ts index 6d7945a425..05c4433c5a 100644 --- a/packages/core-internal/test/wire/schemaTwinConformance.test.ts +++ b/packages/core-internal/test/wire/schemaTwinConformance.test.ts @@ -32,8 +32,8 @@ const FIXTURES_ROOT = join(__dirname, '../corpus/fixtures'); const TWINS_ROOT = join(__dirname, '../corpus/schema-twins'); interface TwinManifest { - source: { repository: string; commit: string }; - files: Record; + source: { repository: string }; + files: Record; } const TWIN_MANIFEST = JSON.parse(readFileSync(join(TWINS_ROOT, 'manifest.json'), 'utf8')) as TwinManifest; @@ -47,6 +47,7 @@ describe('twin provenance integrity (the manifest lock)', () => { // 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]!; + expect(entry.sourceCommit).toMatch(/^[0-9a-f]{40}$/); 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( diff --git a/packages/server/src/server/listenRouter.ts b/packages/server/src/server/listenRouter.ts index 40c4a38cf2..40854df043 100644 --- a/packages/server/src/server/listenRouter.ts +++ b/packages/server/src/server/listenRouter.ts @@ -105,7 +105,7 @@ export interface ListenRouter { * against what the serving instance advertises (honoring a filter without * capabilities would fail open and deliver unadvertised types). * `serverInfo` is the serving instance's identity, stamped onto the - * graceful-close result's `_meta` (the spec's `SubscriptionsListenResultMeta` + * graceful-close result's `_meta` (the spec's `SubscriptionsListenResultMetaObject` * extends `ResultMetaObject`, so the serverInfo SHOULD applies there too). */ serve(message: JSONRPCRequest, signal: AbortSignal | undefined, capabilities: ServerCapabilities, serverInfo: Implementation): Response; @@ -305,7 +305,7 @@ export class StdioListenRouter { private _serverCapabilities: ServerCapabilities | undefined; /** * The serving instance's identity, stamped onto the graceful-close - * results' `_meta` (the spec's `SubscriptionsListenResultMeta` extends + * results' `_meta` (the spec's `SubscriptionsListenResultMetaObject` extends * `ResultMetaObject`). Handed over together with the capabilities. */ private _serverInfo: Implementation | undefined; diff --git a/packages/server/src/server/server.ts b/packages/server/src/server/server.ts index 5de0d8919c..2eee0b9ff4 100644 --- a/packages/server/src/server/server.ts +++ b/packages/server/src/server/server.ts @@ -244,7 +244,7 @@ export function installModernOnlyHandlers(server: Server, servedModernVersions: * Package-internal: the instance's implementation identity, for the serving * entries to stamp onto entry-built results (the `subscriptions/listen` * graceful-close result — built outside the encode seam, but the spec's - * `SubscriptionsListenResultMeta` extends `ResultMetaObject`, so it carries + * `SubscriptionsListenResultMetaObject` extends `ResultMetaObject`, so it carries * the serverInfo SHOULD like every other result). Not public API. */ export function serverIdentityOf(server: Server): Implementation { diff --git a/packages/server/test/server/createMcpHandlerListen.test.ts b/packages/server/test/server/createMcpHandlerListen.test.ts index fe17aa7536..cc0acc6173 100644 --- a/packages/server/test/server/createMcpHandlerListen.test.ts +++ b/packages/server/test/server/createMcpHandlerListen.test.ts @@ -256,7 +256,7 @@ describe('createMcpHandler — subscriptions/listen', () => { _meta: { 'io.modelcontextprotocol/subscriptionId': 1, // #3002: the close result carries the serving instance's - // identity like every other result (SubscriptionsListenResultMeta + // identity like every other result (SubscriptionsListenResultMetaObject // extends ResultMetaObject). 'io.modelcontextprotocol/serverInfo': { name: 'listen-test-server', version: '1.0.0' } } diff --git a/packages/server/test/server/serveStdioListen.test.ts b/packages/server/test/server/serveStdioListen.test.ts index 44deb64396..c084f87169 100644 --- a/packages/server/test/server/serveStdioListen.test.ts +++ b/packages/server/test/server/serveStdioListen.test.ts @@ -122,7 +122,7 @@ describe('serveStdio — subscriptions/listen', () => { const results = inbound.filter(m => 'result' in m) as { id: unknown; result: unknown }[]; expect(results.map(m => m.id)).toEqual(['s1', 's2']); // #3002: each close result carries the serving instance's identity - // (SubscriptionsListenResultMeta extends ResultMetaObject). + // (SubscriptionsListenResultMetaObject extends ResultMetaObject). const serverInfo = { name: 's', version: '1' }; expect(results.map(m => m.result)).toEqual([ { resultType: 'complete', _meta: { [SUBSCRIPTION_ID_META_KEY]: 's1', 'io.modelcontextprotocol/serverInfo': serverInfo } }, diff --git a/scripts/fetch-schema-twins.ts b/scripts/fetch-schema-twins.ts index d693f45905..781d2bc5e6 100644 --- a/scripts/fetch-schema-twins.ts +++ b/scripts/fetch-schema-twins.ts @@ -7,18 +7,15 @@ * 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. + * is locked to the manifest's source commit, sha256, and byte count. * - * Refresh ATOMICALLY with the matching spec.types anchor (see - * packages/core-internal/src/types/README.md lifecycle rule 4). + * Each revision owns its source commit independently. Refreshing one released + * revision must not move another revision's oracle. * * 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. + * pnpm fetch:schema-twins + * pnpm fetch:schema-twins + * pnpm fetch:schema-twins */ import { createHash } from 'node:crypto'; @@ -33,10 +30,17 @@ const SPEC_REPO = 'modelcontextprotocol/modelcontextprotocol'; const TWINS_DIR = join(PROJECT_ROOT, 'packages', 'core-internal', 'test', 'corpus', 'schema-twins'); const MANIFEST_PATH = join(TWINS_DIR, 'manifest.json'); +interface TwinEntry { + sourceCommit: string; + sha256: string; + bytes: number; + upstreamPath: string; +} + interface TwinManifest { comment: string; - source: { repository: string; commit: string }; - files: Record; + source: { repository: string }; + files: Record; } async function fetchRawBytes(sha: string, upstreamPath: string): Promise { @@ -50,19 +54,34 @@ async function fetchRawBytes(sha: string, upstreamPath: string): Promise async function main(): Promise { const manifest = JSON.parse(readFileSync(MANIFEST_PATH, 'utf8')) as TwinManifest; - const sha = process.argv[2] ?? manifest.source.commit; + if (manifest.source.repository !== SPEC_REPO) { + throw new Error(`Unexpected schema-twin source repository: ${manifest.source.repository}`); + } + + const [providedRevision, providedSHA, ...extraArgs] = process.argv.slice(2); + if (extraArgs.length > 0) { + throw new Error('Usage: pnpm fetch:schema-twins [revision] [sha]'); + } + if (providedRevision !== undefined && !(providedRevision in manifest.files)) { + throw new Error( + `Unsupported revision "${providedRevision}". Available revisions: ${Object.keys(manifest.files).join(', ')}` + ); + } for (const [revision, entry] of Object.entries(manifest.files)) { + if (providedRevision !== undefined && revision !== providedRevision) continue; + + const sha = providedSHA ?? entry.sourceCommit; 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.sourceCommit = sha; 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}`); } diff --git a/scripts/fetch-spec-examples.ts b/scripts/fetch-spec-examples.ts index bc279551d9..005627b4c8 100644 --- a/scripts/fetch-spec-examples.ts +++ b/scripts/fetch-spec-examples.ts @@ -1,9 +1,9 @@ /** - * Vendors the draft-revision (2026-07-28) example corpus from the spec - * repository into `packages/core-internal/test/corpus/fixtures/2026-07-28/`. + * Vendors the released 2026-07-28 example corpus from the spec repository + * into `packages/core-internal/test/corpus/fixtures/2026-07-28/`. * - * The spec repository ships canonical example instances for the draft schema - * (`schema/draft/examples//*.json`). The corpus harness + * The spec repository ships canonical example instances for the released schema + * (`schema/2026-07-28/examples//*.json`). The corpus harness * (`packages/core-internal/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. @@ -13,7 +13,7 @@ * * Usage: * pnpm fetch:spec-examples --spec-dir - * pnpm fetch:spec-examples [sha] # fetch from GitHub (default: latest main) + * pnpm fetch:spec-examples [sha] # fetch from GitHub (default: released pin) * * With `--spec-dir`, examples are read from a local checkout of * modelcontextprotocol/modelcontextprotocol (provenance is the checkout's @@ -30,10 +30,10 @@ 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-internal', 'test', 'corpus', 'fixtures', DRAFT_REVISION); +const REVISION = '2026-07-28'; +const RELEASED_REVISION_PIN = '271ecc9accafdd9b83a3c869fa67c22953b2af80'; +const EXAMPLES_PATH = `schema/${REVISION}/examples`; +const OUTPUT_DIR = join(PROJECT_ROOT, 'packages', 'core-internal', 'test', 'corpus', 'fixtures', REVISION); interface ExampleFile { /** `/.json` relative to the examples root. */ @@ -41,15 +41,6 @@ interface ExampleFile { 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); @@ -115,7 +106,7 @@ function writeCorpus(files: ExampleFile[], sha: string): void { } const manifest = { - revision: DRAFT_REVISION, + revision: 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, @@ -139,7 +130,7 @@ async function main(): Promise { return; } - const sha = args[0] ?? (await fetchLatestSHA()); + const sha = args[0] ?? RELEASED_REVISION_PIN; const files = await fetchExamplesFromGitHub(sha); writeCorpus(files, sha); } diff --git a/scripts/fetch-spec-types.ts b/scripts/fetch-spec-types.ts index c4fc9818ec..06b426f23d 100644 --- a/scripts/fetch-spec-types.ts +++ b/scripts/fetch-spec-types.ts @@ -10,38 +10,33 @@ const PROJECT_ROOT = join(__dirname, '..'); /** * The protocol revisions the SDK keeps reference types for: * - `2025-11-25`: the frozen, released schema. - * - `2026-07-28`: the upcoming protocol revision. + * - `2026-07-28`: the frozen, released schema. * * Each is written to `packages/core-internal/src/types/spec.types..ts`. */ const SUPPORTED_VERSIONS = ['2025-11-25', '2026-07-28'] as const; type SpecVersion = (typeof SUPPORTED_VERSIONS)[number]; -/** - * Upstream schema directory per revision. Until the 2026-07-28 revision is - * published, its schema lives in the spec repository's draft directory; this - * mapping drops once `schema/2026-07-28/` exists upstream. - */ +/** Upstream schema directory for each supported protocol revision. */ const UPSTREAM_SCHEMA_DIRS: Record = { '2025-11-25': '2025-11-25', - '2026-07-28': 'draft' + '2026-07-28': '2026-07-28' }; /** * 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`. + * can only change through a deliberate, reviewed repin. * - * Draft-tracking revisions have no entry and float to the latest upstream - * commit via the nightly workflow's refresh PRs. + * Draft-tracking revisions have no entry and may float to the latest upstream + * commit only while the SDK has an explicit consumer for that unreleased revision. * * See `packages/core-internal/src/types/README.md` for the full lifecycle policy. */ const RELEASED_REVISION_PINS: Partial> = { - '2025-11-25': '0168c57fc74aba6e6dcf8f0b7191db3caaa5ad65' + '2025-11-25': '0168c57fc74aba6e6dcf8f0b7191db3caaa5ad65', + '2026-07-28': '271ecc9accafdd9b83a3c869fa67c22953b2af80' }; interface GitHubCommit { @@ -127,7 +122,7 @@ function isSupportedVersion(value: string): value is SpecVersion { async function main() { try { // Usage: fetch-spec-types.ts [version] [sha] - // With no version, all supported versions are fetched at their latest upstream SHA. + // With no version, all supported versions are regenerated from their configured authority. const providedVersion = process.argv[2]; const providedSHA = process.argv[3]; From e2523025aa7993429adae4e46e285a602b61154e Mon Sep 17 00:00:00 2001 From: squarepots <46488165+squarepots@users.noreply.github.com> Date: Thu, 24 Sep 2026 23:43:11 +0800 Subject: [PATCH 2/6] fix(spec): align 2025 anchor provenance with release pin --- packages/core-internal/src/types/spec.types.2025-11-25.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core-internal/src/types/spec.types.2025-11-25.ts b/packages/core-internal/src/types/spec.types.2025-11-25.ts index 225a53c2d7..3cbbc8f6c3 100644 --- a/packages/core-internal/src/types/spec.types.2025-11-25.ts +++ b/packages/core-internal/src/types/spec.types.2025-11-25.ts @@ -3,7 +3,7 @@ * * Source: https://github.com/modelcontextprotocol/modelcontextprotocol * Pulled from: https://raw.githubusercontent.com/modelcontextprotocol/modelcontextprotocol/main/schema/2025-11-25/schema.ts - * Last updated from commit: 357adac47ab2654b64799f994e6db8d3df4ee19d + * Last updated from commit: 0168c57fc74aba6e6dcf8f0b7191db3caaa5ad65 * * DO NOT EDIT THIS FILE MANUALLY. Changes will be overwritten by automated updates. * To update this file, run: pnpm run fetch:spec-types 2025-11-25 From 6fcda04b864c1873759fefd57d60847b65807a8c Mon Sep 17 00:00:00 2001 From: squarepots <46488165+squarepots@users.noreply.github.com> Date: Thu, 24 Sep 2026 23:44:49 +0800 Subject: [PATCH 3/6] style(spec): format 2026 parity check --- packages/core-internal/test/spec.types.2026-07-28.test.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/core-internal/test/spec.types.2026-07-28.test.ts b/packages/core-internal/test/spec.types.2026-07-28.test.ts index bf6ed2613f..eb8f4d6349 100644 --- a/packages/core-internal/test/spec.types.2026-07-28.test.ts +++ b/packages/core-internal/test/spec.types.2026-07-28.test.ts @@ -805,7 +805,10 @@ const wireParityChecks = { sdk = spec; spec = sdk; }, - SubscriptionsListenResultMetaObject: (sdk: WSubscriptionsListenResultMeta, spec: SpecTypes.SubscriptionsListenResultMetaObject) => { + SubscriptionsListenResultMetaObject: ( + sdk: WSubscriptionsListenResultMeta, + spec: SpecTypes.SubscriptionsListenResultMetaObject + ) => { sdk = spec; spec = sdk; }, From b8d48733c287622b6474e422dd9dbb9abf9f3b11 Mon Sep 17 00:00:00 2001 From: squarepots <46488165+squarepots@users.noreply.github.com> Date: Thu, 24 Sep 2026 23:49:21 +0800 Subject: [PATCH 4/6] style(spec): match parity check formatting --- .../core-internal/test/spec.types.2026-07-28.test.ts | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/packages/core-internal/test/spec.types.2026-07-28.test.ts b/packages/core-internal/test/spec.types.2026-07-28.test.ts index eb8f4d6349..be1aa51eab 100644 --- a/packages/core-internal/test/spec.types.2026-07-28.test.ts +++ b/packages/core-internal/test/spec.types.2026-07-28.test.ts @@ -805,17 +805,11 @@ const wireParityChecks = { sdk = spec; spec = sdk; }, - SubscriptionsListenResultMetaObject: ( - sdk: WSubscriptionsListenResultMeta, - spec: SpecTypes.SubscriptionsListenResultMetaObject - ) => { + SubscriptionsListenResultMetaObject: (sdk: WSubscriptionsListenResultMeta, spec: SpecTypes.SubscriptionsListenResultMetaObject) => { sdk = spec; spec = sdk; }, - SubscriptionsListenResultResponse: ( - sdk: WSubscriptionsListenResultResponse, - spec: SpecTypes.SubscriptionsListenResultResponse - ) => { + SubscriptionsListenResultResponse: (sdk: WSubscriptionsListenResultResponse, spec: SpecTypes.SubscriptionsListenResultResponse) => { sdk = spec; spec = sdk; }, From 8abfbf5f80d2f4a02bf9488f63d01900e71ff50c Mon Sep 17 00:00:00 2001 From: squarepots <46488165+squarepots@users.noreply.github.com> Date: Fri, 25 Sep 2026 00:45:17 +0800 Subject: [PATCH 5/6] refactor(spec): narrow release cutover scope --- packages/core-internal/src/types/README.md | 6 ++-- .../src/types/spec.types.2025-11-25.ts | 2 +- .../test/corpus/schema-twins/manifest.json | 10 +++---- scripts/fetch-schema-twins.ts | 29 +++++++++---------- 4 files changed, 22 insertions(+), 25 deletions(-) diff --git a/packages/core-internal/src/types/README.md b/packages/core-internal/src/types/README.md index 374032f85d..c0aba1bd43 100644 --- a/packages/core-internal/src/types/README.md +++ b/packages/core-internal/src/types/README.md @@ -18,7 +18,7 @@ They are reference-only test oracles: the comparison suites in `packages/core-in 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-internal/test/corpus/schema-twins/` (`.schema.json` + `manifest.json` recording each revision's 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` from that revision's pinned source 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 + **This clause is OPERATIVE.** The vendored twins are the per-revision `schema.json` copies under `packages/core-internal/test/corpus/schema-twins/` (`.schema.json` + `manifest.json` recording each twin's 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 refresh the matching upstream + `schema//schema.json` from that twin's recorded source commit and update `manifest.json` in the same change; 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-internal/src/types/spec.types.2025-11-25.ts b/packages/core-internal/src/types/spec.types.2025-11-25.ts index 3cbbc8f6c3..225a53c2d7 100644 --- a/packages/core-internal/src/types/spec.types.2025-11-25.ts +++ b/packages/core-internal/src/types/spec.types.2025-11-25.ts @@ -3,7 +3,7 @@ * * Source: https://github.com/modelcontextprotocol/modelcontextprotocol * Pulled from: https://raw.githubusercontent.com/modelcontextprotocol/modelcontextprotocol/main/schema/2025-11-25/schema.ts - * Last updated from commit: 0168c57fc74aba6e6dcf8f0b7191db3caaa5ad65 + * Last updated from commit: 357adac47ab2654b64799f994e6db8d3df4ee19d * * DO NOT EDIT THIS FILE MANUALLY. Changes will be overwritten by automated updates. * To update this file, run: pnpm run fetch:spec-types 2025-11-25 diff --git a/packages/core-internal/test/corpus/schema-twins/manifest.json b/packages/core-internal/test/corpus/schema-twins/manifest.json index 10be62d587..b2f097ce2c 100644 --- a/packages/core-internal/test/corpus/schema-twins/manifest.json +++ b/packages/core-internal/test/corpus/schema-twins/manifest.json @@ -1,20 +1,20 @@ { - "comment": "Vendored schema.json twins (TEST-ONLY conformance oracles; never bundled, never runtime). RAW upstream bytes - never reformat: each file is locked to its per-revision sourceCommit/sha256/bytes by schemaTwinConformance. Refresh via `pnpm fetch:schema-twins [revision] [sha]` (see packages/core-internal/src/types/README.md lifecycle rule 4).", + "comment": "Vendored schema.json twins (TEST-ONLY conformance oracles; never bundled, never runtime). RAW upstream bytes - never reformat: each file is locked to its sourceCommit/sha256/bytes by schemaTwinConformance. Refresh via `pnpm fetch:schema-twins [sha]`; with no SHA each twin uses its recorded sourceCommit.", "source": { "repository": "modelcontextprotocol/modelcontextprotocol" }, "files": { "2026-07-28": { + "sourceCommit": "271ecc9accafdd9b83a3c869fa67c22953b2af80", "sha256": "ef70b61f99b6d2e5e3b46863822eab08dff6a45bedc7a08914e0e5b133f40203", "bytes": 181474, - "upstreamPath": "schema/2026-07-28/schema.json", - "sourceCommit": "271ecc9accafdd9b83a3c869fa67c22953b2af80" + "upstreamPath": "schema/2026-07-28/schema.json" }, "2025-11-25": { + "sourceCommit": "71e306956a4959c9655e5036be215d41986596e6", "sha256": "7b2d96fd95efd2216aa953606b83f5a740ddeaa5ebd3a5d27b45a8296545a118", "bytes": 174326, - "upstreamPath": "schema/2025-11-25/schema.json", - "sourceCommit": "0168c57fc74aba6e6dcf8f0b7191db3caaa5ad65" + "upstreamPath": "schema/2025-11-25/schema.json" } } } diff --git a/scripts/fetch-schema-twins.ts b/scripts/fetch-schema-twins.ts index 781d2bc5e6..b13ffecdce 100644 --- a/scripts/fetch-schema-twins.ts +++ b/scripts/fetch-schema-twins.ts @@ -9,13 +9,19 @@ * formatting of any kind (the directory is .prettierignore'd) — and each file * is locked to the manifest's source commit, sha256, and byte count. * - * Each revision owns its source commit independently. Refreshing one released - * revision must not move another revision's oracle. + * Each revision records its own source commit. Released revision directories + * can continue to change upstream, so freezing one revision must not move a + * different revision's oracle. + * + * Refresh ATOMICALLY with the matching spec.types anchor (see + * packages/core-internal/src/types/README.md lifecycle rule 4). * * Usage: - * pnpm fetch:schema-twins - * pnpm fetch:schema-twins - * pnpm fetch:schema-twins + * pnpm fetch:schema-twins [sha] # default: each manifest entry's current source commit + * + * Passing a SHA preserves the previous CLI behavior and repins all twins to + * that repository snapshot. With no SHA, each twin regenerates from its own + * recorded source commit. */ import { createHash } from 'node:crypto'; @@ -58,23 +64,14 @@ async function main(): Promise { throw new Error(`Unexpected schema-twin source repository: ${manifest.source.repository}`); } - const [providedRevision, providedSHA, ...extraArgs] = process.argv.slice(2); - if (extraArgs.length > 0) { - throw new Error('Usage: pnpm fetch:schema-twins [revision] [sha]'); - } - if (providedRevision !== undefined && !(providedRevision in manifest.files)) { - throw new Error( - `Unsupported revision "${providedRevision}". Available revisions: ${Object.keys(manifest.files).join(', ')}` - ); - } + const providedSHA = process.argv[2]; for (const [revision, entry] of Object.entries(manifest.files)) { - if (providedRevision !== undefined && revision !== providedRevision) continue; - const sha = providedSHA ?? entry.sourceCommit; 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.sourceCommit = sha; entry.sha256 = createHash('sha256').update(bytes).digest('hex'); From 9b24218c07643ba780669e430358239dda5fbb76 Mon Sep 17 00:00:00 2001 From: squarepots <46488165+squarepots@users.noreply.github.com> Date: Fri, 25 Sep 2026 01:23:07 +0800 Subject: [PATCH 6/6] docs(review): use revision-matched spec authority --- REVIEW.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/REVIEW.md b/REVIEW.md index 9047acb1c2..65b4c35935 100644 --- a/REVIEW.md +++ b/REVIEW.md @@ -32,7 +32,7 @@ review rounds and grows over time. ## Checklist **Protocol & spec** -- Types match [`schema.ts`](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/schema/draft/schema.ts) exactly (optional vs required fields) +- Types match the schema for the protocol revision being changed (`schema//schema.ts` for released revisions; `schema/draft/schema.ts` only for unreleased work) - Correct `ProtocolError` codes (enum `ProtocolErrorCode`); HTTP status codes match spec (e.g., 404 vs 410) - Works for both stdio and Streamable HTTP transports — no transport-specific assumptions - Cross-SDK consistency: check what `python-sdk` does for the same feature @@ -61,7 +61,7 @@ When verifying spec compliance, consult the spec directly rather than relying on - MCP documentation server: `https://modelcontextprotocol.io/mcp` - Full spec text (single file, LLM-friendly): `https://modelcontextprotocol.io/llms-full.txt` — fetch to a temp file and grep for the relevant section -- Schema source of truth: [`schema.ts`](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/schema/draft/schema.ts) +- Schema source of truth: the revision-matched `schema.ts` in `modelcontextprotocol/modelcontextprotocol` (`schema//schema.ts` for released revisions; `schema/draft/schema.ts` only for unreleased work) ## Recurring Catches