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/common/eslint-config/eslint.config.mjs b/common/eslint-config/eslint.config.mjs index fb0b5d3db6..808fe1729d 100644 --- a/common/eslint-config/eslint.config.mjs +++ b/common/eslint-config/eslint.config.mjs @@ -94,6 +94,11 @@ export default defineConfig( 'no-console': 'off' } }, + { + // Ignore build artifacts everywhere (mirrors .prettierignore). A flat-config + // object with only `ignores` is a global ignore; ESLint does not skip dist by default. + ignores: ['**/dist/**', '**/build/**', '**/coverage/**'] + }, { // Ignore generated protocol types everywhere ignores: ['**/spec.types.2025-11-25.ts', '**/spec.types.2026-07-28.ts'] diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts index f99c9bcb8f..551575032d 100644 --- a/docs/.vitepress/config.mts +++ b/docs/.vitepress/config.mts @@ -4,8 +4,14 @@ import { fileURLToPath } from 'node:url'; import { defineConfig, type DefaultTheme } from 'vitepress'; +import { generateLlmsArtifacts } from './llms'; +import { guideSidebar } from './nav'; + const docsDir = resolve(dirname(fileURLToPath(import.meta.url)), '..'); +/** Absolute site URL (origin + base), the single copy every consumer derives from. */ +const siteUrl = 'https://ts.sdk.modelcontextprotocol.io/v2'; + /** * The API Reference sidebar is generated by `pnpm docs:api` (typedoc + typedoc-vitepress-theme) * into docs/api/typedoc-sidebar.json. When it is missing (e.g. `docs:dev` before `docs:api`), @@ -25,7 +31,7 @@ export default defineConfig({ description: 'The TypeScript SDK implementation of the Model Context Protocol specification.', base: '/v2/', srcExclude: ['v1/**', '_meta/**', 'behavior-surface-pins.md'], - sitemap: { hostname: 'https://ts.sdk.modelcontextprotocol.io/v2/' }, + sitemap: { hostname: `${siteUrl}/` }, markdown: { config(md) { // Spec-generated JSDoc (packages/core-internal/src/types/spec.types.*.ts) carries @@ -42,6 +48,9 @@ export default defineConfig({ }; } }, + buildEnd(siteConfig) { + generateLlmsArtifacts(docsDir, siteConfig.outDir, siteUrl); + }, themeConfig: { nav: [ { text: 'Get started', link: '/get-started/first-server', activeMatch: '^/get-started/' }, @@ -52,82 +61,7 @@ export default defineConfig({ { text: 'V1 Docs', link: 'https://ts.sdk.modelcontextprotocol.io/' } ], sidebar: [ - { - text: 'Get started', - items: [ - { text: 'Build a server', link: '/get-started/first-server' }, - { text: 'Plug into a real host', link: '/get-started/real-host' }, - { text: 'Build a client', link: '/get-started/first-client' }, - { text: 'Packages', link: '/get-started/packages' }, - { text: 'Examples', link: '/get-started/examples' } - ] - }, - { - text: 'Servers', - items: [ - { text: 'Tools', link: '/servers/tools' }, - { text: 'Resources', link: '/servers/resources' }, - { text: 'Prompts', link: '/servers/prompts' }, - { text: 'Completion', link: '/servers/completion' }, - { text: 'Logging, progress, cancellation', link: '/servers/logging-progress-cancellation' }, - { text: 'Elicitation', link: '/servers/elicitation' }, - { text: 'Sampling (sunset)', link: '/servers/sampling' }, - { text: 'Input required', link: '/servers/input-required' }, - { text: 'Notifications', link: '/servers/notifications' }, - { text: 'Errors', link: '/servers/errors' } - ] - }, - { - text: 'Serving', - items: [ - { text: 'stdio', link: '/serving/stdio' }, - { text: 'HTTP', link: '/serving/http' }, - { text: 'Express', link: '/serving/express' }, - { text: 'Hono', link: '/serving/hono' }, - { text: 'Fastify', link: '/serving/fastify' }, - { text: 'Web-standard runtimes', link: '/serving/web-standard' }, - { text: 'Sessions, state, scaling', link: '/serving/sessions-state-scaling' }, - { text: 'Authorization', link: '/serving/authorization' }, - { text: 'Legacy clients', link: '/serving/legacy-clients' } - ] - }, - { - text: 'Clients', - items: [ - { text: 'Connect', link: '/clients/connect' }, - { text: 'Calling', link: '/clients/calling' }, - { text: 'Handle server requests', link: '/clients/server-requests' }, - { text: 'Roots (sunset)', link: '/clients/roots' }, - { text: 'Subscriptions', link: '/clients/subscriptions' }, - { text: 'OAuth', link: '/clients/oauth' }, - { text: 'Machine auth', link: '/clients/machine-auth' }, - { text: 'Middleware', link: '/clients/middleware' }, - { text: 'Caching', link: '/clients/caching' } - ] - }, - { text: 'Protocol versions', link: '/protocol-versions' }, - { - text: 'Advanced', - collapsed: true, - items: [ - { text: 'Low-level server', link: '/advanced/low-level-server' }, - { text: 'Custom methods', link: '/advanced/custom-methods' }, - { text: 'Schema libraries', link: '/advanced/schema-libraries' }, - { text: 'Custom transports', link: '/advanced/custom-transports' }, - { text: 'Wire schemas', link: '/advanced/wire-schemas' }, - { text: 'Gateway', link: '/advanced/gateway' } - ] - }, - { text: 'Testing', link: '/testing' }, - { text: 'Troubleshooting', link: '/troubleshooting' }, - { - text: 'Migration', - items: [ - { text: 'Overview', link: '/migration/' }, - { text: 'Upgrade to v2', link: '/migration/upgrade-to-v2' }, - { text: '2026-07-28 protocol support', link: '/migration/support-2026-07-28' } - ] - }, + ...guideSidebar, { text: 'API Reference', collapsed: true, diff --git a/docs/.vitepress/llms.ts b/docs/.vitepress/llms.ts new file mode 100644 index 0000000000..af186fbac9 --- /dev/null +++ b/docs/.vitepress/llms.ts @@ -0,0 +1,213 @@ +import { mkdirSync, readFileSync, writeFileSync } from 'node:fs'; +import { dirname, join, posix } from 'node:path'; + +import type { DefaultTheme } from 'vitepress'; + +import { guideSidebar } from './nav'; + +/** + * LLM-facing renditions of the docs, generated into the site output at build + * time (wired into `buildEnd` in config.mts), following https://llmstxt.org + * and the Python SDK's equivalent: + * + * - `.md` next to every guide page's HTML — the page as plain markdown, + * frontmatter stripped, links absolutized (cross-page links point at the + * `.md` renditions so an agent can keep fetching markdown). + * - `llms.txt` — the index: one line per page, grouped like the sidebar. + * - `llms-full.txt` — every guide page concatenated in sidebar order. + * + * The generated API reference stays out of all three (linked, not inlined): + * it would swamp the prose that answers most questions. + */ + +interface Page { + /** Sidebar title. */ + title: string; + /** Path of the markdown source relative to the docs dir, e.g. `servers/tools.md`. */ + sourcePath: string; + /** Absolute URL of the markdown rendition. */ + mdUrl: string; +} + +interface Section { + title: string; + pages: Page[]; +} + +function toPage(site: string, title: string, route: string): Page { + const sourcePath = route === '/' ? 'index.md' : route.endsWith('/') ? `${route.slice(1)}index.md` : `${route.slice(1)}.md`; + return { title, sourcePath, mdUrl: `${site}/${sourcePath}` }; +} + +/** Flatten the sidebar into ordered sections, preserving sidebar order exactly. */ +function sections(site: string): Section[] { + const out: Section[] = [{ title: 'Overview', pages: [toPage(site, 'MCP TypeScript SDK', '/')] }]; + for (const entry of guideSidebar as (DefaultTheme.SidebarItem & { items?: DefaultTheme.SidebarItem[] })[]) { + if (entry.items) { + out.push({ title: entry.text!, pages: entry.items.map(i => toPage(site, i.text!, i.link!)) }); + } else { + out.push({ title: entry.text!, pages: [toPage(site, entry.text!, entry.link!)] }); + } + } + return out; +} + +/** Read a `description:` frontmatter value, if present. */ +function frontmatterDescription(markdown: string): string | undefined { + if (!markdown.startsWith('---\n')) return undefined; + const end = markdown.indexOf('\n---\n', 4); + if (end === -1) return undefined; + const match = /^description:\s*(.+)$/m.exec(markdown.slice(4, end)); + return match?.[1].trim().replace(/^(['"])(.*)\1$/, '$2'); +} + +/** Strip `--- ... ---` frontmatter. */ +function stripFrontmatter(markdown: string): string { + if (!markdown.startsWith('---\n')) return markdown; + const end = markdown.indexOf('\n---\n', 4); + return end === -1 ? markdown : markdown.slice(end + 5).replace(/^\n+/, ''); +} + +/** Drop the `source="…"` wiring attribute from fence info lines. */ +function stripFenceAttributes(markdown: string): string { + return markdown.replace(/^```(\w+)\s+source="[^"]*"\s*$/gm, '```$1'); +} + +/** + * Absolutize every link for a page living at `pageDir` (docs-relative, '' for + * the root). Relative `.md` links stay `.md` so they resolve to the markdown + * renditions; `/specification/…` goes to modelcontextprotocol.io, mirroring + * the render-time rewrite in config.mts. + */ +function absolutizeLinks(markdown: string, pageDir: string, site: string, sourcePath: string): string { + // Runs over the raw markdown, code fences included — fine while no code + // sample contains a relative markdown link (none do; a false rewrite would + // show up in review of the rendition). + return markdown.replace(/\]\(([^)\s]+)\)/g, (match, target: string) => { + if (/^(https?:|mailto:|#)/.test(target)) return match; + if (target.startsWith('/specification/')) return `](https://modelcontextprotocol.io${target})`; + if (target.startsWith('/')) return `](${site}${target})`; + const hashIndex = target.indexOf('#'); + const path = hashIndex === -1 ? target : target.slice(0, hashIndex); + const hash = hashIndex === -1 ? '' : target.slice(hashIndex); + let resolved = posix.normalize(posix.join(pageDir, path)); + if (resolved.startsWith('..')) { + throw new Error(`${sourcePath}: relative link escapes the docs root: ${target} — use the site URL or a GitHub URL`); + } + if (resolved.startsWith('api/')) { + // The generated API reference has no markdown renditions — link its HTML. + resolved = resolved.replace(/\.md$/, '.html'); + } + return `](${site}/${resolved}${hash})`; + }); +} + +function renderPage(docsDir: string, page: Page, site: string): { markdown: string; description?: string } { + const raw = readFileSync(join(docsDir, page.sourcePath), 'utf8'); + const pageDir = posix.dirname(page.sourcePath); + return { + markdown: absolutizeLinks(stripFenceAttributes(stripFrontmatter(raw)), pageDir === '.' ? '' : pageDir, site, page.sourcePath), + description: frontmatterDescription(raw) + }; +} + +/** First prose sentence after the H1, markdown stripped, for the llms.txt index line. */ +function describe(rendered: string): string { + const lines = rendered.split('\n'); + let inFence = false; + let inContainer = false; + let past = false; + const paragraph: string[] = []; + for (const line of lines) { + if (line.startsWith('```')) { + inFence = !inFence; + continue; + } + if (inFence) continue; + if (line.startsWith(':::')) { + inContainer = line.trim() !== ':::'; + continue; + } + if (inContainer) continue; + if (line.startsWith('# ')) { + past = true; + continue; + } + if (!past) continue; + if (line.trim() === '' || /^[#>|]|^[-*] /.test(line)) { + if (paragraph.length > 0) break; + continue; + } + paragraph.push(line.trim()); + } + const plain = paragraph + .join(' ') + .replace(/\[([^\]]*)\]\([^)]*\)/g, '$1') + .replace(/[*`]/g, '') + .trim(); + const sentence = /^(.*?[.!?])(\s|$)/.exec(plain); + const blurb = sentence ? sentence[1] : plain; + return blurb.length > 220 ? `${blurb.slice(0, 217).replace(/\s+\S*$/, '')}…` : blurb; +} + +/** + * Generate llms.txt, llms-full.txt, and the per-page markdown renditions into + * `outDir`. `site` is the absolute site URL without a trailing slash + * (origin + base), owned by config.mts. + */ +export function generateLlmsArtifacts(docsDir: string, outDir: string, site: string): void { + const groups = sections(site); + const rendered = new Map(); + + for (const section of groups) { + for (const page of section.pages) { + const { markdown, description } = renderPage(docsDir, page, site); + rendered.set(page.sourcePath, { markdown, blurb: description ?? describe(markdown) }); + const outPath = join(outDir, page.sourcePath); + mkdirSync(dirname(outPath), { recursive: true }); + writeFileSync(outPath, markdown); + } + } + + const header = [ + '# MCP TypeScript SDK', + '', + '> The official TypeScript SDK for the Model Context Protocol (MCP): build MCP servers and clients on Node.js, Bun, Deno, and Workers. This is the v2 beta documentation, tracking the 2026-07-28 spec revision.', + '', + 'Every page below is also served as plain markdown at its `.md` URL — fetch any page directly.', + '' + ]; + + const index = [...header]; + for (const section of groups) { + index.push(`## ${section.title}`, ''); + for (const page of section.pages) { + const blurb = rendered.get(page.sourcePath)!.blurb; + index.push(`- [${page.title}](${page.mdUrl})${blurb ? `: ${blurb}` : ''}`); + } + index.push(''); + } + index.push( + '## Optional', + '', + `- [llms-full.txt](${site}/llms-full.txt): every page above concatenated into one file`, + `- [API reference](${site}/api/): generated per-package API reference`, + '- [MCP specification](https://modelcontextprotocol.io/specification/latest)', + '- [v1 documentation](https://ts.sdk.modelcontextprotocol.io/)', + '' + ); + writeFileSync(join(outDir, 'llms.txt'), index.join('\n')); + + // A separator no page body contains ('---' appears in the guides as a + // thematic break, so it cannot delimit pages unambiguously). + const separator = '='.repeat(80); + const full = [...header]; + for (const section of groups) { + for (const page of section.pages) { + full.push(separator, `Source: ${page.mdUrl}`, separator, '', rendered.get(page.sourcePath)!.markdown.trimEnd(), ''); + } + } + writeFileSync(join(outDir, 'llms-full.txt'), full.join('\n')); + + console.log(`[docs] llms.txt, llms-full.txt, and ${rendered.size} markdown renditions written`); +} diff --git a/docs/.vitepress/nav.ts b/docs/.vitepress/nav.ts new file mode 100644 index 0000000000..02ab4d2ff6 --- /dev/null +++ b/docs/.vitepress/nav.ts @@ -0,0 +1,85 @@ +import type { DefaultTheme } from 'vitepress'; + +/** + * The guide sidebar: every hand-written page, in reading order. Shared by the + * site config (which appends the generated API Reference group) and the + * llms.txt generator (which mirrors these groups and this order). + */ +export const guideSidebar: DefaultTheme.SidebarItem[] = [ + { + text: 'Get started', + items: [ + { text: 'Build a server', link: '/get-started/first-server' }, + { text: 'Plug into a real host', link: '/get-started/real-host' }, + { text: 'Build a client', link: '/get-started/first-client' }, + { text: 'Packages', link: '/get-started/packages' }, + { text: 'Examples', link: '/get-started/examples' } + ] + }, + { + text: 'Servers', + items: [ + { text: 'Tools', link: '/servers/tools' }, + { text: 'Resources', link: '/servers/resources' }, + { text: 'Prompts', link: '/servers/prompts' }, + { text: 'Completion', link: '/servers/completion' }, + { text: 'Logging, progress, cancellation', link: '/servers/logging-progress-cancellation' }, + { text: 'Elicitation', link: '/servers/elicitation' }, + { text: 'Sampling (sunset)', link: '/servers/sampling' }, + { text: 'Input required', link: '/servers/input-required' }, + { text: 'Notifications', link: '/servers/notifications' }, + { text: 'Errors', link: '/servers/errors' } + ] + }, + { + text: 'Serving', + items: [ + { text: 'stdio', link: '/serving/stdio' }, + { text: 'HTTP', link: '/serving/http' }, + { text: 'Express', link: '/serving/express' }, + { text: 'Hono', link: '/serving/hono' }, + { text: 'Fastify', link: '/serving/fastify' }, + { text: 'Web-standard runtimes', link: '/serving/web-standard' }, + { text: 'Sessions, state, scaling', link: '/serving/sessions-state-scaling' }, + { text: 'Authorization', link: '/serving/authorization' }, + { text: 'Legacy clients', link: '/serving/legacy-clients' } + ] + }, + { + text: 'Clients', + items: [ + { text: 'Connect', link: '/clients/connect' }, + { text: 'Calling', link: '/clients/calling' }, + { text: 'Handle server requests', link: '/clients/server-requests' }, + { text: 'Roots (sunset)', link: '/clients/roots' }, + { text: 'Subscriptions', link: '/clients/subscriptions' }, + { text: 'OAuth', link: '/clients/oauth' }, + { text: 'Machine auth', link: '/clients/machine-auth' }, + { text: 'Middleware', link: '/clients/middleware' }, + { text: 'Caching', link: '/clients/caching' } + ] + }, + { text: 'Protocol versions', link: '/protocol-versions' }, + { + text: 'Advanced', + collapsed: true, + items: [ + { text: 'Low-level server', link: '/advanced/low-level-server' }, + { text: 'Custom methods', link: '/advanced/custom-methods' }, + { text: 'Schema libraries', link: '/advanced/schema-libraries' }, + { text: 'Custom transports', link: '/advanced/custom-transports' }, + { text: 'Wire schemas', link: '/advanced/wire-schemas' }, + { text: 'Gateway', link: '/advanced/gateway' } + ] + }, + { text: 'Testing', link: '/testing' }, + { text: 'Troubleshooting', link: '/troubleshooting' }, + { + text: 'Migration', + items: [ + { text: 'Overview', link: '/migration/' }, + { text: 'Upgrade to v2', link: '/migration/upgrade-to-v2' }, + { text: '2026-07-28 protocol support', link: '/migration/support-2026-07-28' } + ] + } +]; diff --git a/docs/behavior-surface-pins.md b/docs/behavior-surface-pins.md index df4f171b08..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, ESM-only 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` | +| 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/clients/machine-auth.md b/docs/clients/machine-auth.md index 34d24f8450..1b3c6614bb 100644 --- a/docs/clients/machine-auth.md +++ b/docs/clients/machine-auth.md @@ -1,5 +1,6 @@ --- shape: how-to +description: 'Authenticate a client with no user present: client credentials, private-key JWT, and cross-app access.' --- # Authenticate without a user diff --git a/docs/clients/oauth.md b/docs/clients/oauth.md index ed972919e1..f30dcd9258 100644 --- a/docs/clients/oauth.md +++ b/docs/clients/oauth.md @@ -1,5 +1,6 @@ --- shape: how-to +description: 'Sign an end user in from a client you build with the OAuth authorization-code flow.' --- # Authenticate a user with OAuth diff --git a/docs/migration/upgrade-to-v2.md b/docs/migration/upgrade-to-v2.md index 453eb53dcc..4749f1a8d1 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 . @@ -202,7 +202,7 @@ per-member declarations; per-member is recommended, since the v2 package split m actual needs explicit. To answer "which packages does this member need" directly, run the codemod against that member's directory with `--dry-run`: the manifest summary is computed from that member's own imports. (The authoritative import-path routing lives -in the codemod's [mapping file](../../packages/codemod/src/migrations/v1-to-v2/mappings/importMap.ts).) +in the codemod's [mapping file](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/packages/codemod/src/migrations/v1-to-v2/mappings/importMap.ts).) The framework adapter packages declare their framework as a **peer dependency** (`express`, `hono`, `fastify`); v1 shipped them as direct deps. The codemod adds the @@ -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/docs/serving/authorization.md b/docs/serving/authorization.md index a6163e197f..97f76d1aba 100644 --- a/docs/serving/authorization.md +++ b/docs/serving/authorization.md @@ -1,5 +1,6 @@ --- shape: how-to +description: 'Require a bearer token on a server you run: verification, protected-resource metadata, and per-tool scopes.' --- # Require authorization 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/src/bin/batchTest.ts b/packages/codemod/src/bin/batchTest.ts index 8c71bf7164..1a6802bc86 100644 --- a/packages/codemod/src/bin/batchTest.ts +++ b/packages/codemod/src/bin/batchTest.ts @@ -157,19 +157,30 @@ function detectPm(repoRoot: string): string { return 'npm'; } -function installCommand(pm: string): string { +export function installCommand(pm: string, opts: { hasOwnPnpmWorkspace: boolean; packageDirs: string[] }): string { if (pm !== 'pnpm') return `${pm} install --ignore-scripts`; - // pnpm walks up to find a workspace; clones live inside this SDK's pnpm workspace, so a plain - // `pnpm install` targets the OUTER workspace and never populates the clone's node_modules — every - // downstream check (tsc base config, tsup, vitest) then fails identically at baseline and post, - // masking real codemod signal. - // --ignore-workspace: treat the clone as a standalone project (not part of the SDK workspace). - // --no-frozen-lockfile: the codemod rewrites package.json to swap v1 → v2 deps, so the lockfile - // must be allowed to change. CI=true (set in shell()) otherwise defaults - // pnpm to a frozen lockfile and the post-codemod reinstall silently skips - // the new v2 deps, leaving the clone on v1. - // npm/yarn/bun key off a `workspaces` field in package.json (absent at this repo root), so they - // need no equivalent flags. + // --no-frozen-lockfile: the codemod rewrites package.json to swap v1 → v2 deps, so the lockfile must + // be allowed to change. CI=true (set in shell()) otherwise defaults pnpm to a frozen lockfile and the + // post-codemod reinstall silently skips the new v2 deps, leaving the clone on v1. + if (opts.hasOwnPnpmWorkspace) { + // The clone is its OWN pnpm workspace — pnpm-workspace.yaml defines catalog:/workspace: deps + // (e.g. mastra). `--ignore-workspace` would discard that file and pnpm would fail to resolve them + // (ERR_PNPM_CATALOG_ENTRY_NOT_FOUND_FOR_SPEC; unresolved workspace: links → repo skipped). We don't + // need it: the SDK workspace excludes the clones (`!packages/codemod/batch-test/**`) and pnpm uses + // the clone's own pnpm-workspace.yaml as the nearest root. Scope the install to the target packages + // and their dependencies (`{./}...`) so a monorepo target installs only what the checks need + // (e.g. 2 of mastra's 161 projects) instead of the whole tree. The braces are load-bearing: pnpm + // silently ignores the trailing `...` on a bare `./...` path selector (installing the package + // without its workspace deps); the `{./}...` form honors it. + const filters = opts.packageDirs + .filter(dir => dir !== '.') + .map(dir => `--filter ${JSON.stringify(`{./${dir}}...`)}`) + .join(' '); + return `pnpm install --ignore-scripts --no-frozen-lockfile${filters ? ` ${filters}` : ''}`; + } + // Single-package clone with no workspace of its own: pnpm would walk up to the (clone-excluding) SDK + // workspace and never populate the clone's node_modules. `--ignore-workspace` treats it as standalone. + // npm/yarn/bun key off a `workspaces` field in package.json (absent at this repo root). return 'pnpm install --ignore-scripts --ignore-workspace --no-frozen-lockfile'; } @@ -643,18 +654,23 @@ function main(): void { const pm = detectPm(clonePath); console.log(` Package manager: ${pm}`); - // Step 3: Install + // Process packages + const packages: PackageEntry[] = entry.packages ?? [{ dir: '.', sourceDir: 'src' }]; + const repoPkgResults: PackageReport[] = []; + + // Step 3: Install. A clone that is its own pnpm workspace (catalog:/workspace: deps) must keep its + // pnpm-workspace.yaml — see installCommand. Computed once and reused for the post-codemod reinstall. + const installCmd = installCommand(pm, { + hasOwnPnpmWorkspace: existsSync(path.join(clonePath, 'pnpm-workspace.yaml')), + packageDirs: packages.map(p => p.dir) + }); console.log(' Installing dependencies...'); - const installResult = shell(installCommand(pm), clonePath); + const installResult = shell(installCmd, clonePath); if (installResult.exitCode !== 0) { console.log(` ERROR: install failed, skipping\n ${installResult.stderr.split('\n')[0]}`); continue; } - // Process packages - const packages: PackageEntry[] = entry.packages ?? [{ dir: '.', sourceDir: 'src' }]; - const repoPkgResults: PackageReport[] = []; - for (const pkg of packages) { const sourceDir = pkg.sourceDir ?? 'src'; const fullPkgDir = path.join(clonePath, pkg.dir); @@ -697,7 +713,7 @@ function main(): void { if (rewrites > 0) console.log(` Pinned ${rewrites} deps to resolved published versions`); } console.log(' Re-installing dependencies...'); - shell(installCommand(pm), clonePath); + shell(installCmd, clonePath); // Step 7: Post-codemod checks console.log(' Running post-codemod checks...'); diff --git a/packages/codemod/src/generated/versions.ts b/packages/codemod/src/generated/versions.ts index 196a367508..a82d986db8 100644 --- a/packages/codemod/src/generated/versions.ts +++ b/packages/codemod/src/generated/versions.ts @@ -1,9 +1,9 @@ // AUTO-GENERATED — do not edit. Run `pnpm run generate:versions` to regenerate. export const V2_PACKAGE_VERSIONS: Record = { - '@modelcontextprotocol/client': '^2.0.0-alpha.3', - '@modelcontextprotocol/server': '^2.0.0-alpha.3', - '@modelcontextprotocol/node': '^2.0.0-alpha.3', - '@modelcontextprotocol/express': '^2.0.0-alpha.3', - '@modelcontextprotocol/server-legacy': '^2.0.0-alpha.3', - '@modelcontextprotocol/core': '^2.0.0-alpha.1' + '@modelcontextprotocol/client': '^2.0.0-beta.1', + '@modelcontextprotocol/server': '^2.0.0-beta.1', + '@modelcontextprotocol/node': '^2.0.0-beta.1', + '@modelcontextprotocol/express': '^2.0.0-beta.1', + '@modelcontextprotocol/server-legacy': '^2.0.0-beta.1', + '@modelcontextprotocol/core': '^2.0.0-beta.1' }; diff --git a/packages/codemod/src/migrations/v1-to-v2/transforms/contextTypes.ts b/packages/codemod/src/migrations/v1-to-v2/transforms/contextTypes.ts index dd8abbd137..f0f733ba32 100644 --- a/packages/codemod/src/migrations/v1-to-v2/transforms/contextTypes.ts +++ b/packages/codemod/src/migrations/v1-to-v2/transforms/contextTypes.ts @@ -1,4 +1,4 @@ -import type { SourceFile } from 'ts-morph'; +import type { ObjectLiteralExpression, SourceFile } from 'ts-morph'; import { Node, SyntaxKind } from 'ts-morph'; import type { Diagnostic, Transform, TransformContext, TransformResult } from '../../../types'; @@ -9,8 +9,33 @@ import { CONTEXT_PROPERTY_MAP, CTX_PARAM_NAME, EXTRA_PARAM_NAME } from '../mappi const CONTEXT_LIKE_KEYS = new Set(CONTEXT_PROPERTY_MAP.map(mapping => mapping.from.slice(1))); +/** + * v1 context keys distinctive enough that a single one on an object literal is a strong + * signal it's a hand-built handler-context mock (vs. generic keys like `signal`/`sessionId`/ + * `requestId` — a bare correlation-ID literal such as `logger.info(msg, { requestId })` is + * not a context mock — which appear on unrelated objects and only count in aggregate). + */ +const DISTINCTIVE_CONTEXT_KEYS = new Set([ + 'sendRequest', + 'sendNotification', + 'requestInfo', + 'authInfo', + 'closeSSEStream', + 'closeStandaloneSSEStream' +]); + +/** A literal already carrying one of these is in the v2 nested shape — not a stale v1 mock. */ +const V2_SHAPE_KEYS = new Set(['mcpReq', 'http', 'task']); + const HANDLER_METHODS = new Set(['setRequestHandler', 'setNotificationHandler']); +/** + * Transport ingestion methods whose second argument is a flat `MessageExtraInfo` + * (authInfo/request/closeSSEStream/… stay top-level in v2), NOT a handler context — + * so a literal handed to them must never get handler-context reshape guidance. + */ +const TRANSPORT_MESSAGE_METHODS = new Set(['onmessage', 'handleMessage']); + const REGISTER_METHODS = new Set(['registerTool', 'registerPrompt', 'registerResource', 'tool', 'prompt', 'resource']); /** @@ -303,6 +328,7 @@ export const contextTypesTransform: Transform = { changesCount += processFallbackHandlerAssignments(sourceFile, diagnostics); changesCount += remapAnnotatedContextParams(sourceFile, diagnostics); + flagV1MockContextLiterals(sourceFile, diagnostics); return { changesCount, diagnostics }; } @@ -329,6 +355,108 @@ function processFallbackHandlerAssignments(sourceFile: SourceFile, diagnostics: return changes; } +/** + * Render a v1 context key as a reshape hint, e.g. `sendRequest` → `mcpReq.send`. Returns + * undefined for `sessionId` (a no-op — it stays top-level in v2) and for non-context keys. + */ +function contextKeyReshapeHint(key: string): string | undefined { + const mapping = CONTEXT_PROPERTY_MAP.find(m => m.from === '.' + key); + if (mapping === undefined || mapping.from === mapping.to) return undefined; + // Render the target as a plain object path: '.http?.authInfo' → 'http.authInfo'. + return `${key} → ${mapping.to.replace(/^\./, '').replaceAll('?', '')}`; +} + +/** + * Flag hand-built mocks of the handler context (common in tests). The call-site scan above + * only reshapes `extra.X` inside handler definitions it can anchor on (registerTool, + * setRequestHandler, fallback handlers, annotated params). A test hands its mock to a bare + * `handler(args, mockCtx)` invocation, so the object literal is never reached and keeps the + * flat v1 shape — at runtime the migrated handler reads `ctx.mcpReq.send` / `.id` / … against + * it and throws "Cannot read properties of undefined (reading 'send')". Advisory only: an + * untyped literal that merely shares a key name might not be a context mock, so never rewrite. + */ +function flagV1MockContextLiterals(sourceFile: SourceFile, diagnostics: Diagnostic[]): void { + for (const obj of sourceFile.getDescendantsOfKind(SyntaxKind.ObjectLiteralExpression)) { + // Only literals in a mock-context position: a call argument or a variable initializer. + // Covers both `handler({}, { sendRequest: fn })` and `const extra = { … }; handler(a, extra)`. + // Unwrap casts/parens first so typed mocks — `{ … } as unknown as RequestHandlerExtra`, + // `({ … })`, `{ … } satisfies X` — are anchored by what encloses the cast, not the + // AsExpression that directly wraps the literal. + let expr: Node = obj; + let parent = expr.getParent(); + while ( + parent !== undefined && + (Node.isAsExpression(parent) || Node.isSatisfiesExpression(parent) || Node.isParenthesizedExpression(parent)) + ) { + expr = parent; + parent = parent.getParent(); + } + const isCallArg = parent !== undefined && Node.isCallExpression(parent) && parent.getArguments().includes(expr); + const isVarInit = parent !== undefined && Node.isVariableDeclaration(parent) && parent.getInitializer() === expr; + if (!isCallArg && !isVarInit) continue; + + // A literal handed to a transport ingestion method (`transport.onmessage(msg, { … })`, + // `transport.handleMessage(msg, { … })`) is a flat MessageExtraInfo, not a handler-context + // mock — reshaping its authInfo/request/closeSSEStream/… under http/mcpReq would be wrong. + if (isCallArg && Node.isCallExpression(parent)) { + const callee = parent.getExpression(); + if (Node.isPropertyAccessExpression(callee) && TRANSPORT_MESSAGE_METHODS.has(callee.getName())) continue; + } + + // The codemod's OWN output: an options object inside a just-rewritten handler whose values + // now read `ctx.mcpReq.*` / `ctx.http.*`. The keys keep their v1 names — so V2_SHAPE_KEYS + // below, which only inspects key names, won't catch it — but the values are already v2, not + // a stale mock. Flagging it would insert a comment above code the codemod itself produced. + if (readsFromMigratedContext(obj)) continue; + + // Collect named property keys (skip spreads and computed names). + const keys: string[] = []; + for (const prop of obj.getProperties()) { + if (!Node.isPropertyAssignment(prop) && !Node.isShorthandPropertyAssignment(prop) && !Node.isMethodDeclaration(prop)) continue; + const nameNode = prop.getNameNode(); + if (Node.isIdentifier(nameNode)) keys.push(nameNode.getText()); + else if (Node.isStringLiteral(nameNode)) keys.push(nameNode.getLiteralText()); + } + if (keys.some(key => V2_SHAPE_KEYS.has(key))) continue; // already v2-shaped + + const contextKeys = keys.filter(key => CONTEXT_LIKE_KEYS.has(key)); + const hasDistinctive = contextKeys.some(key => DISTINCTIVE_CONTEXT_KEYS.has(key)); + if (!hasDistinctive && contextKeys.length < 2) continue; + + const reshapes = contextKeys.map(key => contextKeyReshapeHint(key)).filter((hint): hint is string => hint !== undefined); + const sessionNote = contextKeys.includes('sessionId') ? '; sessionId stays top-level' : ''; + diagnostics.push({ + ...actionRequired( + sourceFile.getFilePath(), + obj, + `This object looks like a v1 handler-context mock (${contextKeys.join(', ')}). v2 nests the context — ` + + `reshape it (${reshapes.join('; ')}${sessionNote}), e.g. { sendRequest: fn } → { mcpReq: { send: fn } }. ` + + `Passed as-is to a migrated handler that reads ctx.mcpReq.*, the v1 shape throws ` + + `"Cannot read properties of undefined".` + ), + advisoryOnly: true + }); + } +} + +/** + * True when any property value already reads from the v2-nested context — a `.mcpReq` or `.http` + * property access (e.g. `ctx.mcpReq.signal`, `ctx.http?.authInfo`). Such a literal is migrated + * output, not a hand-built v1 mock: a real v1 mock supplies raw values (`fn`, `ac.signal`, a + * literal), never the nested v2 shape the codemod itself just wrote. + */ +function readsFromMigratedContext(obj: ObjectLiteralExpression): boolean { + for (const prop of obj.getProperties()) { + if (!Node.isPropertyAssignment(prop)) continue; + const init = prop.getInitializer(); + if (init === undefined) continue; + const accesses = init.getDescendantsOfKind(SyntaxKind.PropertyAccessExpression); + if (Node.isPropertyAccessExpression(init)) accesses.push(init); + if (accesses.some(access => access.getName() === 'mcpReq' || access.getName() === 'http')) return true; + } + return false; +} + const CONTEXT_TYPE_NAMES = new Set(['RequestHandlerExtra', 'ServerContext', 'ClientContext']); /** * Split a type's text on top-level `|` only (angle brackets tracked). Returns diff --git a/packages/codemod/src/migrations/v1-to-v2/transforms/handlerRegistration.ts b/packages/codemod/src/migrations/v1-to-v2/transforms/handlerRegistration.ts index 1ecfa00ce8..9c0ff40af7 100644 --- a/packages/codemod/src/migrations/v1-to-v2/transforms/handlerRegistration.ts +++ b/packages/codemod/src/migrations/v1-to-v2/transforms/handlerRegistration.ts @@ -173,6 +173,54 @@ export const handlerRegistrationTransform: Transform = { removeUnusedImport(sourceFile, schemaName, true); } + // Flag surviving registration-schema references. A method-mapped *RequestSchema/*NotificationSchema + // whose import survived the conversion above, used as a VALUE (a call argument or an `===`/`!==` + // operand), is almost always a stale v1 registration assertion/lookup — in v2 the registration key is + // the method string, not the schema. Advisory, not rewritten: the same constant is still valid for + // `.parse()` etc., so we flag rather than risk breaking a legitimate schema use. + // + // Gate on a registration MOCK/lookup, not on any setRequestHandler access. A file that only *calls* + // setRequestHandler/setNotificationHandler (real registrations, which the pass above rewrites) tells us + // nothing about its other schema references — those may be plain schema uses (`validateSchema(S)`, + // `zodToJsonSchema(S)`, `ajv.compile(S)`). Only a NON-call reference — `inner.setRequestHandler.mock`, + // `expect(x.setRequestHandler)…`, a comparison operand — signals the file makes registration + // assertions, where a surviving schema key is likely stale. + const usesRegistrationMock = sourceFile.getDescendantsOfKind(SyntaxKind.PropertyAccessExpression).some(pa => { + if (pa.getName() !== 'setRequestHandler' && pa.getName() !== 'setNotificationHandler') return false; + const paParent = pa.getParent(); + // Exclude real registration calls `x.setRequestHandler(…)`, where pa is the callee. + return !(Node.isCallExpression(paParent) && paParent.getExpression() === pa); + }); + if (usesRegistrationMock) { + const flagged = new Set(); + for (const id of sourceFile.getDescendantsOfKind(SyntaxKind.Identifier)) { + const local = id.getText(); + if (flagged.has(local)) continue; + const original = resolveOriginalImportName(sourceFile, local) ?? local; + const method = ALL_SCHEMA_TO_METHOD[original]; + if (method === undefined || !isImportedFromMcp(sourceFile, local)) continue; + const parent = id.getParent(); + if (parent === undefined) continue; + const isCallArg = Node.isCallExpression(parent) && parent.getArguments().includes(id); + const isComparand = + Node.isBinaryExpression(parent) && + ['===', '!==', '==', '!='].includes(parent.getOperatorToken().getText()) && + (parent.getLeft() === id || parent.getRight() === id); + if (!isCallArg && !isComparand) continue; + flagged.add(local); + diagnostics.push({ + ...actionRequired( + sourceFile.getFilePath(), + id, + `${local} is no longer the setRequestHandler/setNotificationHandler key in v2 — handlers ` + + `register by the method string '${method}'. Update registration assertions/lookups ` + + `(e.g. against a setRequestHandler mock) to compare against '${method}'.` + ), + advisoryOnly: true + }); + } + } + return { changesCount, diagnostics }; } }; diff --git a/packages/codemod/test/batchTest.test.ts b/packages/codemod/test/batchTest.test.ts index 41ea0e9c61..2d470f027a 100644 --- a/packages/codemod/test/batchTest.test.ts +++ b/packages/codemod/test/batchTest.test.ts @@ -3,7 +3,7 @@ import { tmpdir } from 'node:os'; import path from 'node:path'; import { describe, it, expect, vi, afterEach } from 'vitest'; -import { parseArgs, parseCodemodCliOutput, runCodemod } from '../src/bin/batchTest'; +import { parseArgs, parseCodemodCliOutput, runCodemod, installCommand } from '../src/bin/batchTest'; import { computeResultsDirName, type ResolvedConfig } from '../src/bin/batchTest'; import { parseNpmViewVersion, rewriteToPublishedVersion, cleanSubprocessEnv } from '../src/bin/batchTest'; import { getMigration } from '../src/migrations/index'; @@ -12,6 +12,35 @@ afterEach(() => { vi.restoreAllMocks(); }); +describe('installCommand', () => { + it('uses plain --ignore-scripts for non-pnpm managers', () => { + expect(installCommand('npm', { hasOwnPnpmWorkspace: false, packageDirs: ['.'] })).toBe('npm install --ignore-scripts'); + expect(installCommand('yarn', { hasOwnPnpmWorkspace: true, packageDirs: ['packages/a'] })).toBe('yarn install --ignore-scripts'); + }); + + it('isolates a pnpm clone with no workspace of its own via --ignore-workspace', () => { + expect(installCommand('pnpm', { hasOwnPnpmWorkspace: false, packageDirs: ['.'] })).toBe( + 'pnpm install --ignore-scripts --ignore-workspace --no-frozen-lockfile' + ); + }); + + it('respects a pnpm clone that is its own workspace and scopes the install to the target packages', () => { + const cmd = installCommand('pnpm', { hasOwnPnpmWorkspace: true, packageDirs: ['packages/core', 'packages/mcp'] }); + expect(cmd).not.toContain('--ignore-workspace'); + // braces required: pnpm ignores the `...` on a bare `./dir...` selector (drops workspace deps); + // `{./dir}...` includes them. + expect(cmd).toBe( + 'pnpm install --ignore-scripts --no-frozen-lockfile --filter "{./packages/core}..." --filter "{./packages/mcp}..."' + ); + }); + + it('installs the whole workspace (no --filter) when the only target is the clone root', () => { + expect(installCommand('pnpm', { hasOwnPnpmWorkspace: true, packageDirs: ['.'] })).toBe( + 'pnpm install --ignore-scripts --no-frozen-lockfile' + ); + }); +}); + describe('parseArgs', () => { it('defaults to local/local with latest codemod version and unset sdk version', () => { const opts = parseArgs([]); diff --git a/packages/codemod/test/v1-to-v2/transforms/contextTypes.test.ts b/packages/codemod/test/v1-to-v2/transforms/contextTypes.test.ts index 4b41562c1e..b530dc4b34 100644 --- a/packages/codemod/test/v1-to-v2/transforms/contextTypes.test.ts +++ b/packages/codemod/test/v1-to-v2/transforms/contextTypes.test.ts @@ -2,7 +2,7 @@ import { describe, it, expect } from 'vitest'; import { Project } from 'ts-morph'; import { contextTypesTransform } from '../../../src/migrations/v1-to-v2/transforms/contextTypes'; -import type { TransformContext } from '../../../src/types'; +import type { Diagnostic, TransformContext } from '../../../src/types'; const ctx: TransformContext = { projectType: 'server' }; @@ -637,3 +637,121 @@ describe('fallback handler and annotated context params (B5, #152)', () => { expect(diag?.advisoryOnly).toBe(true); }); }); + +describe('v1 mock context literals in tests (test doubles)', () => { + function apply(code: string) { + const project = new Project({ useInMemoryFileSystem: true }); + const sourceFile = project.createSourceFile('test.ts', code); + const result = contextTypesTransform.apply(sourceFile, { projectType: 'server' }); + return { text: sourceFile.getFullText(), result }; + } + const IMPORT = `import { McpServer } from '@modelcontextprotocol/server';\n`; + const findMockDiag = (diags: Diagnostic[]) => diags.find(d => d.message.includes('v1 handler-context mock')); + + it('advises (without rewriting) on a v1 context mock passed inline as a call argument', () => { + const code = IMPORT + [`const r = await handler({}, { sendRequest: mockSendRequest });`, ''].join('\n'); + const { text, result } = apply(code); + // Advisory only — the literal is left untouched. + expect(text).toContain('{ sendRequest: mockSendRequest }'); + const diag = findMockDiag(result.diagnostics); + expect(diag).toBeDefined(); + expect(diag?.advisoryOnly).toBe(true); + expect(diag?.message).toContain('sendRequest → mcpReq.send'); + }); + + it('advises on a v1 context mock bound to a variable, and notes sessionId stays top-level', () => { + const code = IMPORT + [`const extra = { sessionId: 'session-1', sendRequest: fn };`, `await handler(args, extra);`, ''].join('\n'); + const { result } = apply(code); + const diag = findMockDiag(result.diagnostics); + expect(diag).toBeDefined(); + expect(diag?.advisoryOnly).toBe(true); + expect(diag?.message).toContain('sessionId stays top-level'); + }); + + it('advises on the _meta/requestId progress-notification mock shape', () => { + const code = IMPORT + [`await handler({ duration: 1 }, { _meta: {}, requestId: 'test-123' });`, ''].join('\n'); + const { result } = apply(code); + const diag = findMockDiag(result.diagnostics); + expect(diag).toBeDefined(); + expect(diag?.message).toContain('requestId → mcpReq.id'); + }); + + it('advises when two generic context keys appear together (no distinctive key)', () => { + const code = IMPORT + [`await handler(args, { signal: ac.signal, sessionId: 's' });`, ''].join('\n'); + const { result } = apply(code); + expect(findMockDiag(result.diagnostics)).toBeDefined(); + }); + + it('does not advise on an already-migrated v2 context mock', () => { + const code = IMPORT + [`await handler({}, { mcpReq: { send: fn } });`, ''].join('\n'); + const { result } = apply(code); + expect(findMockDiag(result.diagnostics)).toBeUndefined(); + }); + + it('does not advise on a non-context object literal', () => { + const code = IMPORT + [`const config = { title: 'x', description: 'y' };`, ''].join('\n'); + const { result } = apply(code); + expect(findMockDiag(result.diagnostics)).toBeUndefined(); + }); + + it('does not advise on a lone generic key (needs a distinctive key or >=2 context keys)', () => { + const code = IMPORT + [`doThing({ signal: ac.signal });`, ''].join('\n'); + const { result } = apply(code); + expect(findMockDiag(result.diagnostics)).toBeUndefined(); + }); + + it('does not advise on a lone requestId (a bare correlation-ID literal, not a context mock)', () => { + const code = IMPORT + [`logger.info('handling lookup', { requestId: args.id });`, ''].join('\n'); + const { result } = apply(code); + expect(findMockDiag(result.diagnostics)).toBeUndefined(); + }); + + it('advises on a cast-wrapped inline mock (`{ … } as unknown as RequestHandlerExtra`)', () => { + const code = IMPORT + [`await handler(args, { sendRequest: fn } as unknown as RequestHandlerExtra);`, ''].join('\n'); + const { result } = apply(code); + const diag = findMockDiag(result.diagnostics); + expect(diag).toBeDefined(); + expect(diag?.advisoryOnly).toBe(true); + }); + + it('advises on a cast-wrapped variable mock (`const ctx = { … } as any`)', () => { + const code = IMPORT + [`const mockCtx = { requestId: 1, sendRequest: fn } as any;`, `await handler(args, mockCtx);`, ''].join('\n'); + const { result } = apply(code); + expect(findMockDiag(result.diagnostics)).toBeDefined(); + }); + + it('does not advise on already-v2 values (an options object reading ctx.mcpReq / ctx.sessionId)', () => { + const code = IMPORT + [`doThing({ signal: ctx.mcpReq.signal, sessionId: ctx.sessionId });`, ''].join('\n'); + const { result } = apply(code); + expect(findMockDiag(result.diagnostics)).toBeUndefined(); + }); + + it("does not re-flag the codemod's own migrated handler output", () => { + const code = + IMPORT + + [ + `server.setRequestHandler('x', async (req, extra) => {`, + ` await loadData(req.uri, { signal: extra.signal, sessionId: extra.sessionId });`, + ` return {};`, + `});`, + '' + ].join('\n'); + const { text, result } = apply(code); + // sanity: the handler body WAS migrated to the v2 nested shape … + expect(text).toContain('ctx.mcpReq.signal'); + // … and the resulting options object is not mistaken for a stale v1 mock. + expect(findMockDiag(result.diagnostics)).toBeUndefined(); + }); + + it('does not advise on a flat MessageExtraInfo literal passed to onmessage', () => { + const code = IMPORT + [`transport.onmessage(msg, { requestInfo: req, authInfo });`, ''].join('\n'); + const { result } = apply(code); + expect(findMockDiag(result.diagnostics)).toBeUndefined(); + }); + + it('does not advise on a flat MessageExtraInfo literal passed to handleMessage', () => { + const code = IMPORT + [`await transport.handleMessage(msg, { authInfo, closeSSEStream: fn });`, ''].join('\n'); + const { result } = apply(code); + expect(findMockDiag(result.diagnostics)).toBeUndefined(); + }); +}); diff --git a/packages/codemod/test/v1-to-v2/transforms/handlerRegistration.test.ts b/packages/codemod/test/v1-to-v2/transforms/handlerRegistration.test.ts index b29f1cefcb..6cf7f136d1 100644 --- a/packages/codemod/test/v1-to-v2/transforms/handlerRegistration.test.ts +++ b/packages/codemod/test/v1-to-v2/transforms/handlerRegistration.test.ts @@ -394,3 +394,72 @@ describe('variable-hop guard rails (B6 review)', () => { expect(result.diagnostics.some(d => d.message.includes('Custom method handler'))).toBe(true); }); }); + +describe('stale registration-schema references (analysis: mcp-servers/memory)', () => { + function applyWithDiagnostics(code: string) { + const project = new Project({ useInMemoryFileSystem: true }); + const sourceFile = project.createSourceFile('test.ts', code); + const result = handlerRegistrationTransform.apply(sourceFile, { projectType: 'server' }); + return { text: sourceFile.getFullText(), result }; + } + + it('flags a request schema used as a setRequestHandler-mock assertion arg with the method string', () => { + const code = [ + `import { SubscribeRequestSchema } from '@modelcontextprotocol/sdk/types.js';`, + `const schemas = inner.setRequestHandler.mock.calls.map((c) => c[0]);`, + `expect(schemas).toContain(SubscribeRequestSchema);`, + '' + ].join('\n'); + const { result } = applyWithDiagnostics(code); + const diag = result.diagnostics.find( + d => d.message.includes('SubscribeRequestSchema') && d.message.includes("'resources/subscribe'") + ); + expect(diag).toBeDefined(); + }); + + it('flags a schema passed to a registration-lookup helper', () => { + const code = [ + `import { UnsubscribeRequestSchema } from '@modelcontextprotocol/sdk/types.js';`, + `const c = inner.setRequestHandler.mock.calls.find((x) => x[0] === s);`, + `handlerFor(inner, UnsubscribeRequestSchema);`, + '' + ].join('\n'); + const { result } = applyWithDiagnostics(code); + expect(result.diagnostics.some(d => d.message.includes("'resources/unsubscribe'"))).toBe(true); + }); + + it('does NOT flag a request schema used as a schema (.parse) — property-access base', () => { + const code = [ + `import { CallToolRequestSchema } from '@modelcontextprotocol/sdk/types.js';`, + `server.setRequestHandler(CallToolRequestSchema, async () => ({ content: [] }));`, + `const parsed = CallToolRequestSchema.parse(x);`, + '' + ].join('\n'); + const { result } = applyWithDiagnostics(code); + expect(result.diagnostics.some(d => d.message.includes('no longer the setRequestHandler'))).toBe(false); + }); + + it('does NOT flag in a file that performs no handler registration', () => { + const code = [ + `import { SubscribeRequestSchema } from '@modelcontextprotocol/sdk/types.js';`, + `someValidator(SubscribeRequestSchema);`, + '' + ].join('\n'); + const { result } = applyWithDiagnostics(code); + expect(result.diagnostics.some(d => d.message.includes('no longer the setRequestHandler'))).toBe(false); + }); + + it('does NOT flag a schema consumed by a validator when setRequestHandler is only a real registration', () => { + // The file registers a handler for real (a call the pass rewrites); an unrelated schema-consuming + // call like validateSchema(S) / zodToJsonSchema(S) is not a stale registration key — only a + // setRequestHandler MOCK/lookup reference signals that surviving schema refs are stale. + const code = [ + `import { CallToolRequestSchema, SubscribeRequestSchema } from '@modelcontextprotocol/sdk/types.js';`, + `server.setRequestHandler(CallToolRequestSchema, async () => ({ content: [] }));`, + `validateSchema(SubscribeRequestSchema);`, + '' + ].join('\n'); + const { result } = applyWithDiagnostics(code); + expect(result.diagnostics.some(d => d.message.includes('no longer the setRequestHandler'))).toBe(false); + }); +}); 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-internal/test/packageTopologyPins.test.ts b/packages/core-internal/test/packageTopologyPins.test.ts index 4df0188232..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,16 +76,32 @@ describe('public package topology', () => { expect(Object.keys(manifest.exports ?? {})).toEqual(expected.exportKeys); }); - test('ships ESM only', () => { + 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'); - // 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)]) - : []; + 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 ?? {})) { - expect(conditionsOf(entry)).not.toContain('require'); + assertDual(entry); } }); 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,