From f7e03ee282ce4ef9506c3c404944e7eb5e27765e Mon Sep 17 00:00:00 2001 From: Matt Carey Date: Wed, 1 Jul 2026 15:57:17 +0100 Subject: [PATCH 1/3] feat(packaging): ship CommonJS builds alongside ESM for v2 packages Add dual CJS+ESM output to all nine publishable packages via tsdown format: ['esm', 'cjs'] with fixedExtension, and add a require export condition (plus main) so require('@modelcontextprotocol/...') works from CommonJS consumers. Output extensions are normalized across packages (core moves from .js/.d.ts to .mjs/.d.mts); public import paths are unchanged. --- .changeset/cjs-support-v2-packages.md | 18 +++++ packages/client/package.json | 81 ++++++++++++++++---- packages/client/tsdown.config.ts | 3 +- packages/codemod/package.json | 11 ++- packages/codemod/tsdown.config.ts | 3 +- packages/core/package.json | 13 +++- packages/core/tsdown.config.ts | 3 +- packages/middleware/express/package.json | 11 ++- packages/middleware/express/tsdown.config.ts | 3 +- packages/middleware/fastify/package.json | 11 ++- packages/middleware/fastify/tsdown.config.ts | 3 +- packages/middleware/hono/package.json | 11 ++- packages/middleware/hono/tsdown.config.ts | 3 +- packages/middleware/node/package.json | 11 ++- packages/middleware/node/tsdown.config.ts | 3 +- packages/server-legacy/package.json | 31 ++++++-- packages/server-legacy/tsdown.config.ts | 3 +- packages/server/package.json | 81 ++++++++++++++++---- packages/server/tsdown.config.ts | 3 +- 19 files changed, 246 insertions(+), 60 deletions(-) create mode 100644 .changeset/cjs-support-v2-packages.md diff --git a/.changeset/cjs-support-v2-packages.md b/.changeset/cjs-support-v2-packages.md new file mode 100644 index 0000000000..3541e0122b --- /dev/null +++ b/.changeset/cjs-support-v2-packages.md @@ -0,0 +1,18 @@ +--- +'@modelcontextprotocol/server': patch +'@modelcontextprotocol/client': patch +'@modelcontextprotocol/core': patch +'@modelcontextprotocol/server-legacy': patch +'@modelcontextprotocol/codemod': patch +'@modelcontextprotocol/express': patch +'@modelcontextprotocol/hono': patch +'@modelcontextprotocol/fastify': patch +'@modelcontextprotocol/node': patch +--- + +Ship CommonJS builds alongside ESM. Each package now emits both `.mjs`/`.d.mts` +and `.cjs`/`.d.cts` (via tsdown `format: ['esm', 'cjs']`), and its `exports` map +adds a `require` condition so `require('@modelcontextprotocol/…')` works from +CommonJS consumers. Output extensions are normalized across all packages +(`@modelcontextprotocol/core` moves from `.js`/`.d.ts` to `.mjs`/`.d.mts`); the +public import paths are unchanged. diff --git a/packages/client/package.json b/packages/client/package.json index fe7834bb4b..bade926d9d 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -21,40 +21,89 @@ ], "exports": { ".": { - "types": "./dist/index.d.mts", - "import": "./dist/index.mjs" + "import": { + "types": "./dist/index.d.mts", + "default": "./dist/index.mjs" + }, + "require": { + "types": "./dist/index.d.cts", + "default": "./dist/index.cjs" + } }, "./stdio": { - "types": "./dist/stdio.d.mts", - "import": "./dist/stdio.mjs" + "import": { + "types": "./dist/stdio.d.mts", + "default": "./dist/stdio.mjs" + }, + "require": { + "types": "./dist/stdio.d.cts", + "default": "./dist/stdio.cjs" + } }, "./validators/ajv": { - "types": "./dist/validators/ajv.d.mts", - "import": "./dist/validators/ajv.mjs" + "import": { + "types": "./dist/validators/ajv.d.mts", + "default": "./dist/validators/ajv.mjs" + }, + "require": { + "types": "./dist/validators/ajv.d.cts", + "default": "./dist/validators/ajv.cjs" + } }, "./validators/cf-worker": { - "types": "./dist/validators/cfWorker.d.mts", - "import": "./dist/validators/cfWorker.mjs" + "import": { + "types": "./dist/validators/cfWorker.d.mts", + "default": "./dist/validators/cfWorker.mjs" + }, + "require": { + "types": "./dist/validators/cfWorker.d.cts", + "default": "./dist/validators/cfWorker.cjs" + } }, "./_shims": { "workerd": { - "types": "./dist/shimsWorkerd.d.mts", - "import": "./dist/shimsWorkerd.mjs" + "import": { + "types": "./dist/shimsWorkerd.d.mts", + "default": "./dist/shimsWorkerd.mjs" + }, + "require": { + "types": "./dist/shimsWorkerd.d.cts", + "default": "./dist/shimsWorkerd.cjs" + } }, "browser": { - "types": "./dist/shimsBrowser.d.mts", - "import": "./dist/shimsBrowser.mjs" + "import": { + "types": "./dist/shimsBrowser.d.mts", + "default": "./dist/shimsBrowser.mjs" + }, + "require": { + "types": "./dist/shimsBrowser.d.cts", + "default": "./dist/shimsBrowser.cjs" + } }, "node": { - "types": "./dist/shimsNode.d.mts", - "import": "./dist/shimsNode.mjs" + "import": { + "types": "./dist/shimsNode.d.mts", + "default": "./dist/shimsNode.mjs" + }, + "require": { + "types": "./dist/shimsNode.d.cts", + "default": "./dist/shimsNode.cjs" + } }, "default": { - "types": "./dist/shimsNode.d.mts", - "import": "./dist/shimsNode.mjs" + "import": { + "types": "./dist/shimsNode.d.mts", + "default": "./dist/shimsNode.mjs" + }, + "require": { + "types": "./dist/shimsNode.d.cts", + "default": "./dist/shimsNode.cjs" + } } } }, + "main": "./dist/index.cjs", "types": "./dist/index.d.mts", "typesVersions": { "*": { diff --git a/packages/client/tsdown.config.ts b/packages/client/tsdown.config.ts index f0d1949e49..948cf95c88 100644 --- a/packages/client/tsdown.config.ts +++ b/packages/client/tsdown.config.ts @@ -11,7 +11,8 @@ export default defineConfig({ 'src/validators/ajv.ts', 'src/validators/cfWorker.ts' ], - format: ['esm'], + format: ['esm', 'cjs'], + fixedExtension: true, outDir: 'dist', clean: true, sourcemap: true, diff --git a/packages/codemod/package.json b/packages/codemod/package.json index 7fa24d9d30..0552fff7b3 100644 --- a/packages/codemod/package.json +++ b/packages/codemod/package.json @@ -25,10 +25,17 @@ }, "exports": { ".": { - "types": "./dist/index.d.mts", - "import": "./dist/index.mjs" + "import": { + "types": "./dist/index.d.mts", + "default": "./dist/index.mjs" + }, + "require": { + "types": "./dist/index.d.cts", + "default": "./dist/index.cjs" + } } }, + "main": "./dist/index.cjs", "files": [ "dist" ], diff --git a/packages/codemod/tsdown.config.ts b/packages/codemod/tsdown.config.ts index 21ae185dba..44976b8256 100644 --- a/packages/codemod/tsdown.config.ts +++ b/packages/codemod/tsdown.config.ts @@ -3,7 +3,8 @@ import { defineConfig } from 'tsdown'; export default defineConfig({ failOnWarn: 'ci-only', entry: ['src/cli.ts', 'src/index.ts'], - format: ['esm'], + format: ['esm', 'cjs'], + fixedExtension: true, outDir: 'dist', clean: true, sourcemap: true, diff --git a/packages/core/package.json b/packages/core/package.json index 75d62a92bf..7a6f115e48 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -22,11 +22,18 @@ ], "exports": { ".": { - "types": "./dist/index.d.ts", - "import": "./dist/index.js" + "import": { + "types": "./dist/index.d.mts", + "default": "./dist/index.mjs" + }, + "require": { + "types": "./dist/index.d.cts", + "default": "./dist/index.cjs" + } } }, - "types": "./dist/index.d.ts", + "main": "./dist/index.cjs", + "types": "./dist/index.d.mts", "files": [ "dist" ], diff --git a/packages/core/tsdown.config.ts b/packages/core/tsdown.config.ts index 464ae98615..b138a07cce 100644 --- a/packages/core/tsdown.config.ts +++ b/packages/core/tsdown.config.ts @@ -12,7 +12,8 @@ import { defineConfig } from 'tsdown'; export default defineConfig({ failOnWarn: 'ci-only', entry: ['src/index.ts'], - format: ['esm'], + format: ['esm', 'cjs'], + fixedExtension: true, outDir: 'dist', clean: true, sourcemap: true, diff --git a/packages/middleware/express/package.json b/packages/middleware/express/package.json index f1bbe44dc0..52c99d40a2 100644 --- a/packages/middleware/express/package.json +++ b/packages/middleware/express/package.json @@ -23,10 +23,17 @@ ], "exports": { ".": { - "types": "./dist/index.d.mts", - "import": "./dist/index.mjs" + "import": { + "types": "./dist/index.d.mts", + "default": "./dist/index.mjs" + }, + "require": { + "types": "./dist/index.d.cts", + "default": "./dist/index.cjs" + } } }, + "main": "./dist/index.cjs", "types": "./dist/index.d.mts", "files": [ "dist" diff --git a/packages/middleware/express/tsdown.config.ts b/packages/middleware/express/tsdown.config.ts index c67ee585cd..5fb5f7b39b 100644 --- a/packages/middleware/express/tsdown.config.ts +++ b/packages/middleware/express/tsdown.config.ts @@ -3,7 +3,8 @@ import { defineConfig } from 'tsdown'; export default defineConfig({ failOnWarn: 'ci-only', entry: ['src/index.ts'], - format: ['esm'], + format: ['esm', 'cjs'], + fixedExtension: true, outDir: 'dist', clean: true, sourcemap: true, diff --git a/packages/middleware/fastify/package.json b/packages/middleware/fastify/package.json index e7f916e8de..84b1c91ec4 100644 --- a/packages/middleware/fastify/package.json +++ b/packages/middleware/fastify/package.json @@ -23,10 +23,17 @@ ], "exports": { ".": { - "types": "./dist/index.d.mts", - "import": "./dist/index.mjs" + "import": { + "types": "./dist/index.d.mts", + "default": "./dist/index.mjs" + }, + "require": { + "types": "./dist/index.d.cts", + "default": "./dist/index.cjs" + } } }, + "main": "./dist/index.cjs", "types": "./dist/index.d.mts", "files": [ "dist" diff --git a/packages/middleware/fastify/tsdown.config.ts b/packages/middleware/fastify/tsdown.config.ts index c67ee585cd..5fb5f7b39b 100644 --- a/packages/middleware/fastify/tsdown.config.ts +++ b/packages/middleware/fastify/tsdown.config.ts @@ -3,7 +3,8 @@ import { defineConfig } from 'tsdown'; export default defineConfig({ failOnWarn: 'ci-only', entry: ['src/index.ts'], - format: ['esm'], + format: ['esm', 'cjs'], + fixedExtension: true, outDir: 'dist', clean: true, sourcemap: true, diff --git a/packages/middleware/hono/package.json b/packages/middleware/hono/package.json index 52b5dfb657..e53942f33b 100644 --- a/packages/middleware/hono/package.json +++ b/packages/middleware/hono/package.json @@ -23,10 +23,17 @@ ], "exports": { ".": { - "types": "./dist/index.d.mts", - "import": "./dist/index.mjs" + "import": { + "types": "./dist/index.d.mts", + "default": "./dist/index.mjs" + }, + "require": { + "types": "./dist/index.d.cts", + "default": "./dist/index.cjs" + } } }, + "main": "./dist/index.cjs", "types": "./dist/index.d.mts", "files": [ "dist" diff --git a/packages/middleware/hono/tsdown.config.ts b/packages/middleware/hono/tsdown.config.ts index c67ee585cd..5fb5f7b39b 100644 --- a/packages/middleware/hono/tsdown.config.ts +++ b/packages/middleware/hono/tsdown.config.ts @@ -3,7 +3,8 @@ import { defineConfig } from 'tsdown'; export default defineConfig({ failOnWarn: 'ci-only', entry: ['src/index.ts'], - format: ['esm'], + format: ['esm', 'cjs'], + fixedExtension: true, outDir: 'dist', clean: true, sourcemap: true, diff --git a/packages/middleware/node/package.json b/packages/middleware/node/package.json index 678dc89a60..e8b22a4472 100644 --- a/packages/middleware/node/package.json +++ b/packages/middleware/node/package.json @@ -22,10 +22,17 @@ ], "exports": { ".": { - "types": "./dist/index.d.mts", - "import": "./dist/index.mjs" + "import": { + "types": "./dist/index.d.mts", + "default": "./dist/index.mjs" + }, + "require": { + "types": "./dist/index.d.cts", + "default": "./dist/index.cjs" + } } }, + "main": "./dist/index.cjs", "types": "./dist/index.d.mts", "files": [ "dist" diff --git a/packages/middleware/node/tsdown.config.ts b/packages/middleware/node/tsdown.config.ts index b6081802ff..2af784601f 100644 --- a/packages/middleware/node/tsdown.config.ts +++ b/packages/middleware/node/tsdown.config.ts @@ -7,7 +7,8 @@ export default defineConfig({ entry: ['src/index.ts'], // 2. Output Configuration - format: ['esm'], + format: ['esm', 'cjs'], + fixedExtension: true, outDir: 'dist', clean: true, // Recommended: Cleans 'dist' before building sourcemap: true, diff --git a/packages/server-legacy/package.json b/packages/server-legacy/package.json index e2fcf3f63a..ef2e8cf608 100644 --- a/packages/server-legacy/package.json +++ b/packages/server-legacy/package.json @@ -26,18 +26,37 @@ ], "exports": { ".": { - "types": "./dist/index.d.mts", - "import": "./dist/index.mjs" + "import": { + "types": "./dist/index.d.mts", + "default": "./dist/index.mjs" + }, + "require": { + "types": "./dist/index.d.cts", + "default": "./dist/index.cjs" + } }, "./sse": { - "types": "./dist/sse/index.d.mts", - "import": "./dist/sse/index.mjs" + "import": { + "types": "./dist/sse/index.d.mts", + "default": "./dist/sse/index.mjs" + }, + "require": { + "types": "./dist/sse/index.d.cts", + "default": "./dist/sse/index.cjs" + } }, "./auth": { - "types": "./dist/auth/index.d.mts", - "import": "./dist/auth/index.mjs" + "import": { + "types": "./dist/auth/index.d.mts", + "default": "./dist/auth/index.mjs" + }, + "require": { + "types": "./dist/auth/index.d.cts", + "default": "./dist/auth/index.cjs" + } } }, + "main": "./dist/index.cjs", "types": "./dist/index.d.mts", "typesVersions": { "*": { diff --git a/packages/server-legacy/tsdown.config.ts b/packages/server-legacy/tsdown.config.ts index 2fff3afcd7..b4c2881fd1 100644 --- a/packages/server-legacy/tsdown.config.ts +++ b/packages/server-legacy/tsdown.config.ts @@ -3,7 +3,8 @@ import { defineConfig } from 'tsdown'; export default defineConfig({ failOnWarn: 'ci-only', entry: ['src/index.ts', 'src/sse/index.ts', 'src/auth/index.ts'], - format: ['esm'], + format: ['esm', 'cjs'], + fixedExtension: true, outDir: 'dist', clean: true, sourcemap: true, diff --git a/packages/server/package.json b/packages/server/package.json index d2d5d72b5a..9a0750ca45 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -21,40 +21,89 @@ ], "exports": { ".": { - "types": "./dist/index.d.mts", - "import": "./dist/index.mjs" + "import": { + "types": "./dist/index.d.mts", + "default": "./dist/index.mjs" + }, + "require": { + "types": "./dist/index.d.cts", + "default": "./dist/index.cjs" + } }, "./stdio": { - "types": "./dist/stdio.d.mts", - "import": "./dist/stdio.mjs" + "import": { + "types": "./dist/stdio.d.mts", + "default": "./dist/stdio.mjs" + }, + "require": { + "types": "./dist/stdio.d.cts", + "default": "./dist/stdio.cjs" + } }, "./validators/ajv": { - "types": "./dist/validators/ajv.d.mts", - "import": "./dist/validators/ajv.mjs" + "import": { + "types": "./dist/validators/ajv.d.mts", + "default": "./dist/validators/ajv.mjs" + }, + "require": { + "types": "./dist/validators/ajv.d.cts", + "default": "./dist/validators/ajv.cjs" + } }, "./validators/cf-worker": { - "types": "./dist/validators/cfWorker.d.mts", - "import": "./dist/validators/cfWorker.mjs" + "import": { + "types": "./dist/validators/cfWorker.d.mts", + "default": "./dist/validators/cfWorker.mjs" + }, + "require": { + "types": "./dist/validators/cfWorker.d.cts", + "default": "./dist/validators/cfWorker.cjs" + } }, "./_shims": { "workerd": { - "types": "./dist/shimsWorkerd.d.mts", - "import": "./dist/shimsWorkerd.mjs" + "import": { + "types": "./dist/shimsWorkerd.d.mts", + "default": "./dist/shimsWorkerd.mjs" + }, + "require": { + "types": "./dist/shimsWorkerd.d.cts", + "default": "./dist/shimsWorkerd.cjs" + } }, "browser": { - "types": "./dist/shimsWorkerd.d.mts", - "import": "./dist/shimsWorkerd.mjs" + "import": { + "types": "./dist/shimsWorkerd.d.mts", + "default": "./dist/shimsWorkerd.mjs" + }, + "require": { + "types": "./dist/shimsWorkerd.d.cts", + "default": "./dist/shimsWorkerd.cjs" + } }, "node": { - "types": "./dist/shimsNode.d.mts", - "import": "./dist/shimsNode.mjs" + "import": { + "types": "./dist/shimsNode.d.mts", + "default": "./dist/shimsNode.mjs" + }, + "require": { + "types": "./dist/shimsNode.d.cts", + "default": "./dist/shimsNode.cjs" + } }, "default": { - "types": "./dist/shimsNode.d.mts", - "import": "./dist/shimsNode.mjs" + "import": { + "types": "./dist/shimsNode.d.mts", + "default": "./dist/shimsNode.mjs" + }, + "require": { + "types": "./dist/shimsNode.d.cts", + "default": "./dist/shimsNode.cjs" + } } } }, + "main": "./dist/index.cjs", "types": "./dist/index.d.mts", "typesVersions": { "*": { diff --git a/packages/server/tsdown.config.ts b/packages/server/tsdown.config.ts index 3ae3158242..adec842c2f 100644 --- a/packages/server/tsdown.config.ts +++ b/packages/server/tsdown.config.ts @@ -10,7 +10,8 @@ export default defineConfig({ 'src/validators/ajv.ts', 'src/validators/cfWorker.ts' ], - format: ['esm'], + format: ['esm', 'cjs'], + fixedExtension: true, outDir: 'dist', clean: true, sourcemap: true, From c4138a039143fd989a39f695007d2989ae205acf Mon Sep 17 00:00:00 2001 From: Matt Carey Date: Wed, 1 Jul 2026 16:34:49 +0100 Subject: [PATCH 2/3] test: drop ESM-only export pin now that v2 packages ship CJS packageTopologyPins pinned the packages as ESM-only (no 'require' export condition). This PR intentionally adds CJS, so remove that assertion; the name/export-key/files/bin pins in the file stay. Update the surface-pins doc row to match. --- docs/behavior-surface-pins.md | 2 +- .../core-internal/test/packageTopologyPins.test.ts | 13 ------------- 2 files changed, 1 insertion(+), 14 deletions(-) diff --git a/docs/behavior-surface-pins.md b/docs/behavior-surface-pins.md index df4f171b08..9eb3e11459 100644 --- a/docs/behavior-surface-pins.md +++ b/docs/behavior-surface-pins.md @@ -27,7 +27,7 @@ CI pass — that reopens the silent-drift hole the pin exists to close. | --- | --- | | Wire error-code tables, error classes, version constants | `packages/core-internal/test/types/errorSurfacePins.test.ts` | | Schema strict/strip/loose boundaries, key existence | `packages/core-internal/test/types/schemaBoundaryPins.test.ts` | -| Published package set, export maps, ESM-only topology | `packages/core-internal/test/packageTopologyPins.test.ts` | +| Published package set, export maps | `packages/core-internal/test/packageTopologyPins.test.ts` | | stdio environment-inheritance safelist | `packages/client/test/client/stdioEnvPins.test.ts` | | 2025-11-25 wire method-registry membership, schema identity | `packages/core-internal/test/types/registryPins.test.ts` | diff --git a/packages/core-internal/test/packageTopologyPins.test.ts b/packages/core-internal/test/packageTopologyPins.test.ts index 4df0188232..6eb28fc6e5 100644 --- a/packages/core-internal/test/packageTopologyPins.test.ts +++ b/packages/core-internal/test/packageTopologyPins.test.ts @@ -75,19 +75,6 @@ describe('public package topology', () => { 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']); }); From c3241d8018cc15b5939dab0dd7521855e8ca62ee Mon Sep 17 00:00:00 2001 From: Matt Carey Date: Wed, 1 Jul 2026 17:04:13 +0100 Subject: [PATCH 3/3] test(packaging): pin dual ESM/CJS surface; docs: reflect CJS support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Address review on #2405: - Replace the removed 'ships ESM only' pin with a 'ships dual ESM + CJS' pin in packageTopologyPins.test.ts: asserts type:'module', a .cjs main, and that every export leaf (recursively, incl. _shims runtime conditions) offers both import and require branches — so a future drop of either branch fails CI instead of silently shipping. - Update docs/migration/upgrade-to-v2.md: v2 now ships CJS, so require() resolves natively; drop the obsolete Jest moduleNameMapper workaround and the ESM-only claims. - docs/behavior-surface-pins.md: describe the pinned surface as dual ESM/CJS. --- docs/behavior-surface-pins.md | 14 ++--- docs/migration/upgrade-to-v2.md | 57 +++++-------------- .../test/packageTopologyPins.test.ts | 30 ++++++++++ 3 files changed, 52 insertions(+), 49 deletions(-) diff --git a/docs/behavior-surface-pins.md b/docs/behavior-surface-pins.md index 9eb3e11459..70257c9015 100644 --- a/docs/behavior-surface-pins.md +++ b/docs/behavior-surface-pins.md @@ -23,13 +23,13 @@ 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-internal/test/types/errorSurfacePins.test.ts` | -| Schema strict/strip/loose boundaries, key existence | `packages/core-internal/test/types/schemaBoundaryPins.test.ts` | -| Published package set, export maps | `packages/core-internal/test/packageTopologyPins.test.ts` | -| stdio environment-inheritance safelist | `packages/client/test/client/stdioEnvPins.test.ts` | -| 2025-11-25 wire method-registry membership, schema identity | `packages/core-internal/test/types/registryPins.test.ts` | +| Surface | File | +| ----------------------------------------------------------- | -------------------------------------------------------------- | +| Wire error-code tables, error classes, version constants | `packages/core-internal/test/types/errorSurfacePins.test.ts` | +| Schema strict/strip/loose boundaries, key existence | `packages/core-internal/test/types/schemaBoundaryPins.test.ts` | +| Published package set, export maps, dual ESM/CJS topology | `packages/core-internal/test/packageTopologyPins.test.ts` | +| stdio environment-inheritance safelist | `packages/client/test/client/stdioEnvPins.test.ts` | +| 2025-11-25 wire method-registry membership, schema identity | `packages/core-internal/test/types/registryPins.test.ts` | ## Writing a new pin diff --git a/docs/migration/upgrade-to-v2.md b/docs/migration/upgrade-to-v2.md index 453eb53dcc..08c6a3e953 100644 --- a/docs/migration/upgrade-to-v2.md +++ b/docs/migration/upgrade-to-v2.md @@ -15,8 +15,8 @@ If you are already on v2 and want to adopt the **2026-07-28 protocol revision**, ## TL;DR — quick path -1. **Prerequisites.** Node.js 20+ and ESM (`"type": "module"` or `.mts`). v2 ships ESM - only; CommonJS callers must use dynamic `import()`. +1. **Prerequisites.** Node.js 20+. v2 is ESM-first but ships a CommonJS build too, so + both `import` and `require('@modelcontextprotocol/…')` resolve natively. 2. **Run the codemod.** ```bash npx @modelcontextprotocol/codemod@beta v1-to-v2 . @@ -212,8 +212,9 @@ depends on `@hono/node-server` at runtime (Node HTTP ↔ Web Standard conversion does **not** require the `hono` framework — your package manager may emit a harmless unmet-peer warning for `hono` (upstream `@hono/node-server` declares it). -v2 requires **Node.js 20+** and ships **ESM only**. If your project uses CommonJS -(`require()`), either migrate to ESM or use dynamic `import()`. +v2 requires **Node.js 20+**. It is ESM-first but ships a **CommonJS build alongside +ESM**, so CommonJS projects can `require('@modelcontextprotocol/…')` directly — no +dynamic `import()` shim required. Repo-local tooling that encodes the literal v1 package name — dependency-pin lints, version allowlists, CI checks, scripts — fails after the manifest swap and is invisible @@ -225,13 +226,13 @@ directly, and a surviving cast keeps suppressing type checking that would otherw catch real errors. Tooling that pins SDK **dist text** (reading a constant out of a built file with -`require.resolve` + a regex) breaks in three stacked ways: the v2 exports maps offer -nothing a CJS `require.resolve` can find; the literal usually lives in a +`require.resolve` + a regex) breaks in two stacked ways: the literal usually lives in a content-hashed sibling chunk (`dist/sse-.mjs`), not the subpath's entry module, so fixed-path reads do not survive a rebuild — scan the package's `dist/` directory for the literal instead; and the emitted quote style differs from v1, so a -quote-anchored pattern misses silently — match either quote. v2 also ships ESM only: -`/dist/cjs/` ↔ `/dist/esm/` flavor-pair path swaps have no equivalent. +quote-anchored pattern misses silently — match either quote. The build layout also +changed: v2 emits `.mjs`/`.cjs` siblings in a flat `dist/`, so v1's `/dist/cjs/` ↔ +`/dist/esm/` flavor-pair path swaps have no equivalent. #### Registry availability during the beta @@ -250,41 +251,13 @@ window: (`pnpm install && pnpm build`, then `pnpm pack` in the package directory) and reference it with a committed `file:` dependency. -#### CommonJS test runners (Jest) cannot resolve the v2 packages - -Every leaf of the v2 packages' `exports` maps carries only the `types` and `import` -conditions — there is no `require` or `default` leaf — so the packages cannot be -resolved by CJS resolvers at all. Jest under its default CommonJS resolution (including -`next/jest` setups) fails with `Cannot find module '@modelcontextprotocol/client'` even -when a transform that handles ESM is configured: resolution fails before any transform -runs. Vitest and native Node ESM are unaffected. - -The interim recipe — interim because the packaging shape is still under discussion and -a later alpha may make it unnecessary — maps the bare specifiers straight to the dist -ESM files and lets the transform convert them (the dists contain no `import.meta`, so -an ESM→CJS transform such as SWC or Babel handles them cleanly): - -```js -// jest.config.js -transformIgnorePatterns: [], // or a pattern that still transforms @modelcontextprotocol/* -moduleNameMapper: { - '^@modelcontextprotocol/client$': '/node_modules/@modelcontextprotocol/client/dist/index.mjs', - '^@modelcontextprotocol/server$': '/node_modules/@modelcontextprotocol/server/dist/index.mjs', - // `_shims` is the packages' internal runtime-selection self-reference; - // pin it to the Node build under jest. - '^@modelcontextprotocol/client/_shims$': '/node_modules/@modelcontextprotocol/client/dist/shimsNode.mjs', - '^@modelcontextprotocol/server/_shims$': '/node_modules/@modelcontextprotocol/server/dist/shimsNode.mjs', -}, -``` +#### CommonJS test runners (Jest) -The entries are exact-anchored — add one per subpath you import (`/stdio` → -`dist/stdio.mjs`, `/validators/cf-worker` → `dist/validators/cfWorker.mjs`) and one for -`@modelcontextprotocol/core` (`dist/index.js`) if you import the raw schemas. The -`_shims` mappings are required whenever the matching root mapping is present: the dist -entry files import `@modelcontextprotocol/client/_shims` (a package self-reference) -internally, and that specifier fails CJS resolution the same way. In a hoisted -monorepo, point the paths at the `node_modules` directory your package manager actually -installs into. +v2 ships a CommonJS build, so CJS test runners resolve the packages natively through the +`require` export condition — Jest (including `next/jest` setups) no longer needs a +`moduleNameMapper` workaround to import `@modelcontextprotocol/*`. If you carried a +v1-era mapping that pinned these packages to their `dist/*.mjs` files, remove it. Vitest +and native Node ESM are unaffected. #### Bundlers: nested `zod` copies in zod@3-pinned monorepos diff --git a/packages/core-internal/test/packageTopologyPins.test.ts b/packages/core-internal/test/packageTopologyPins.test.ts index 6eb28fc6e5..e6f16758f1 100644 --- a/packages/core-internal/test/packageTopologyPins.test.ts +++ b/packages/core-internal/test/packageTopologyPins.test.ts @@ -23,6 +23,7 @@ interface PackageManifest { name: string; private?: boolean; type?: string; + main?: string; files?: string[]; bin?: Record; exports?: Record; @@ -75,6 +76,35 @@ describe('public package topology', () => { expect(Object.keys(manifest.exports ?? {})).toEqual(expected.exportKeys); }); + test('ships dual ESM + CJS', () => { + // The v2 packages are ESM-first but ship a CommonJS build too, so + // require('@modelcontextprotocol/…') resolves natively. Pin that + // deliberate dual surface: type stays 'module', a `.cjs` main is + // present for bare-require fallback, and every export condition + // that resolves a module format offers BOTH an `import` (ESM) and a + // `require` (CJS) branch — recursively, so nested runtime conditions + // (e.g. ./_shims → workerd/browser/node/default) are covered. + expect(manifest.type).toBe('module'); + expect(manifest.main).toMatch(/\.cjs$/); + + const assertDual = (node: unknown): void => { + if (node === null || typeof node !== 'object') { + return; + } + const keys = Object.keys(node); + if (keys.includes('import') || keys.includes('require')) { + expect(keys).toContain('import'); + expect(keys).toContain('require'); + } + for (const value of Object.values(node)) { + assertDual(value); + } + }; + for (const entry of Object.values(manifest.exports ?? {})) { + assertDual(entry); + } + }); + test('publishes only dist', () => { expect(manifest.files).toEqual(['dist']); });