From ada352f353a45c3e56b6b3a59f7f01202b1f46a7 Mon Sep 17 00:00:00 2001 From: Konstantin Konstantinov Date: Tue, 23 Jun 2026 08:56:03 +0300 Subject: [PATCH 01/21] codemod iterations --- docs/migration-SKILL.md | 3 +- docs/migration.md | 2 + packages/codemod/batch-test/repos.json | 40 ++---- packages/codemod/src/cli.ts | 27 ++++ packages/codemod/src/utils/detectFormatter.ts | 113 +++++++++++++++++ packages/codemod/test/detectFormatter.test.ts | 119 ++++++++++++++++++ 6 files changed, 270 insertions(+), 34 deletions(-) create mode 100644 packages/codemod/src/utils/detectFormatter.ts create mode 100644 packages/codemod/test/detectFormatter.test.ts diff --git a/docs/migration-SKILL.md b/docs/migration-SKILL.md index b849da8b3d..5c4dc5e76b 100644 --- a/docs/migration-SKILL.md +++ b/docs/migration-SKILL.md @@ -549,4 +549,5 @@ Validator behavior: 8. If using server SSE transport, migrate to Streamable HTTP 9. If using server auth from the SDK: RS helpers (`requireBearerAuth`, `mcpAuthMetadataRouter`, `OAuthTokenVerifier`) → `@modelcontextprotocol/express`; AS helpers → `@modelcontextprotocol/server-legacy/auth` (deprecated); migrate AS to external IdP/OAuth library 10. If relying on `listTools()`/`listPrompts()`/etc. throwing on missing capabilities, set `enforceStrictCapabilities: true` -11. Verify: build with `tsc` / run tests +11. Format the changed files with the project's formatter (`prettier --write`, `eslint --fix`, or `biome format --write`) — edits are not reformatted automatically, and the wrapped schemas (step 5) and rewritten `setRequestHandler` method strings (section 9) frequently need it to satisfy lint +12. Verify: build with `tsc` / run tests diff --git a/docs/migration.md b/docs/migration.md index bf88cf2b76..cc9caeed9b 100644 --- a/docs/migration.md +++ b/docs/migration.md @@ -7,6 +7,8 @@ This guide covers the breaking changes introduced in v2 of the MCP TypeScript SD Version 2 of the MCP TypeScript SDK introduces several breaking changes to improve modularity, reduce dependency bloat, and provide a cleaner API surface. The biggest change is the split from a single `@modelcontextprotocol/sdk` package into separate `@modelcontextprotocol/core`, `@modelcontextprotocol/client`, and `@modelcontextprotocol/server` packages. +> **Formatting:** The `@modelcontextprotocol/codemod` package automates most of the mechanical changes below, but it rewrites your code's AST without reformatting it — wrapped schemas and generated handler method strings may not match your project's style. After migrating (with the codemod or by hand), run your formatter on the changed files — for example `prettier --write`, `eslint --fix`, or `biome format --write` — and review the diff. + ## Breaking Changes ### Package split (monorepo) diff --git a/packages/codemod/batch-test/repos.json b/packages/codemod/batch-test/repos.json index e8d5515800..d7faa64e2c 100644 --- a/packages/codemod/batch-test/repos.json +++ b/packages/codemod/batch-test/repos.json @@ -1,42 +1,16 @@ [ { - "repo": "modelcontextprotocol/servers", - "ref": "main", + "repo": "upstash/context7", + "ref": "master", "packages": [ { - "dir": "src/everything", - "sourceDir": ".", - "checks": { - "typecheck": "npx tsc --noEmit", - "build": "npm run build", - "test": "npm run test", - "lint": "npm run prettier:check" - } - } - ] - }, - { - "repo": "modelcontextprotocol/inspector", - "ref": "main", - "packages": [ - { - "dir": "client", - "sourceDir": "src", - "checks": { - "typecheck": "npx tsc --noEmit", - "build": "npm run build", - "test": "npm run test", - "lint": "npm run lint" - } - }, - { - "dir": "server", + "dir": "packages/mcp", "sourceDir": "src", "checks": { - "typecheck": "npx tsc --noEmit", - "build": "npm run build", - "test": null, - "lint": null + "typecheck": "pnpm run typecheck", + "build": "pnpm run build", + "test": "pnpm run test", + "lint": "pnpm run lint" } } ] diff --git a/packages/codemod/src/cli.ts b/packages/codemod/src/cli.ts index d8599c70a0..22f878ddd5 100644 --- a/packages/codemod/src/cli.ts +++ b/packages/codemod/src/cli.ts @@ -9,11 +9,36 @@ import { Command } from 'commander'; import { listMigrations } from './migrations/index.js'; import { run } from './runner.js'; import { DiagnosticLevel } from './types.js'; +import { detectFormatter } from './utils/detectFormatter.js'; import { CODEMOD_ERROR_PREFIX, formatDiagnostic } from './utils/diagnostics.js'; const require = createRequire(import.meta.url); const { version } = require('../package.json') as { version: string }; +function quoteArg(arg: string): string { + return /\s/.test(arg) ? `"${arg}"` : arg; +} + +/** + * The codemod transforms the AST but does not reformat — wrapped schemas and + * generated string literals can violate a repo's lint/formatting rules. Point + * the user at their own formatter (which respects their config) for the exact + * files that changed. + */ +function printFormatGuidance(targetDir: string, changedFiles: string[]): void { + if (changedFiles.length === 0) return; + + const formatter = detectFormatter(targetDir); + const fileArgs = changedFiles.map(file => quoteArg(path.relative(process.cwd(), file) || file)); + + console.log("This codemod doesn't reformat its output. Run your formatter on the changed file(s):"); + if (formatter) { + console.log(` ${formatter.bin} ${[...formatter.writeArgs, ...fileArgs].join(' ')}\n`); + } else { + console.log(` e.g. prettier --write ${fileArgs.join(' ')}\n`); + } +} + const program = new Command(); program.name('mcp-codemod').description('Codemod to migrate MCP TypeScript SDK code between versions').version(version); @@ -150,6 +175,8 @@ for (const [name, migration] of listMigrations()) { if (opts['dryRun']) { console.log('Run without --dry-run to apply changes.\n'); } else { + const changedFiles = result.fileResults.filter(fr => fr.changes > 0).map(fr => fr.filePath); + printFormatGuidance(resolvedDir, changedFiles); if (result.packageJsonChanges) { console.log('Run your package manager to install the new packages.\n'); } diff --git a/packages/codemod/src/utils/detectFormatter.ts b/packages/codemod/src/utils/detectFormatter.ts new file mode 100644 index 0000000000..29d584706a --- /dev/null +++ b/packages/codemod/src/utils/detectFormatter.ts @@ -0,0 +1,113 @@ +import { existsSync, readFileSync } from 'node:fs'; +import path from 'node:path'; + +/** A code formatter the codemod can recommend running after a migration. */ +export interface DetectedFormatter { + /** Display name, e.g. `Prettier`. */ + name: string; + /** Executable name, e.g. `prettier`. */ + bin: string; + /** Arguments that write formatting in place; changed file paths are appended after these. */ + writeArgs: readonly string[]; +} + +const BIOME_CONFIG_FILES = ['biome.json', 'biome.jsonc']; +const PRETTIER_CONFIG_FILES = [ + '.prettierrc', + '.prettierrc.json', + '.prettierrc.json5', + '.prettierrc.yaml', + '.prettierrc.yml', + '.prettierrc.toml', + '.prettierrc.js', + '.prettierrc.cjs', + '.prettierrc.mjs', + '.prettierrc.ts', + 'prettier.config.js', + 'prettier.config.cjs', + 'prettier.config.mjs', + 'prettier.config.ts' +]; +const ESLINT_CONFIG_FILES = [ + 'eslint.config.js', + 'eslint.config.mjs', + 'eslint.config.cjs', + 'eslint.config.ts', + 'eslint.config.mts', + 'eslint.config.cts', + '.eslintrc', + '.eslintrc.js', + '.eslintrc.cjs', + '.eslintrc.json', + '.eslintrc.yml', + '.eslintrc.yaml' +]; + +// Precedence order: a configured dedicated formatter wins over ESLint's --fix. +const FORMATTERS = { + biome: { name: 'Biome', bin: 'biome', writeArgs: ['format', '--write'] }, + prettier: { name: 'Prettier', bin: 'prettier', writeArgs: ['--write'] }, + eslint: { name: 'ESLint', bin: 'eslint', writeArgs: ['--fix'] } +} as const satisfies Record; + +function hasAnyFile(dir: string, files: readonly string[]): boolean { + return files.some(file => existsSync(path.join(dir, file))); +} + +interface PackageJsonSignals { + prettier: boolean; + eslint: boolean; +} + +function readPackageJsonSignals(dir: string): PackageJsonSignals { + const pkgJsonPath = path.join(dir, 'package.json'); + if (!existsSync(pkgJsonPath)) return { prettier: false, eslint: false }; + try { + const pkgJson = JSON.parse(readFileSync(pkgJsonPath, 'utf8')) as Record; + const allDeps = { + ...(pkgJson.dependencies as Record | undefined), + ...(pkgJson.devDependencies as Record | undefined) + }; + return { + prettier: 'prettier' in pkgJson || 'prettier' in allDeps, + eslint: 'eslint' in allDeps + }; + } catch { + return { prettier: false, eslint: false }; + } +} + +/** + * Walks up from `startDir` — bounded to the repository, stopping at a `.git` + * directory so a global config in `$HOME` is never matched — looking for a + * configured code formatter, so the CLI can suggest the right "format your + * changed files" command after a migration. + * + * Detection is config-based and runs nothing. When multiple formatters are + * configured, precedence is Biome > Prettier > ESLint. + * + * @returns the detected formatter, or `null` if none is configured. + */ +export function detectFormatter(startDir: string): DetectedFormatter | null { + let dir = path.resolve(startDir); + const root = path.parse(dir).root; + const found = { biome: false, prettier: false, eslint: false }; + + while (true) { + if (hasAnyFile(dir, BIOME_CONFIG_FILES)) found.biome = true; + if (hasAnyFile(dir, PRETTIER_CONFIG_FILES)) found.prettier = true; + if (hasAnyFile(dir, ESLINT_CONFIG_FILES)) found.eslint = true; + + const signals = readPackageJsonSignals(dir); + if (signals.prettier) found.prettier = true; + if (signals.eslint) found.eslint = true; + + if (existsSync(path.join(dir, '.git')) || dir === root) break; + dir = path.dirname(dir); + } + + if (found.biome) return FORMATTERS.biome; + if (found.prettier) return FORMATTERS.prettier; + if (found.eslint) return FORMATTERS.eslint; + return null; +} diff --git a/packages/codemod/test/detectFormatter.test.ts b/packages/codemod/test/detectFormatter.test.ts new file mode 100644 index 0000000000..4958815949 --- /dev/null +++ b/packages/codemod/test/detectFormatter.test.ts @@ -0,0 +1,119 @@ +import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import path from 'node:path'; +import { describe, it, expect, afterEach } from 'vitest'; + +import { detectFormatter } from '../src/utils/detectFormatter.js'; + +let tempDir: string; + +function createTempDir(): string { + tempDir = mkdtempSync(path.join(tmpdir(), 'mcp-codemod-formatter-')); + return tempDir; +} + +function writePkg(dir: string, pkg: Record): void { + writeFileSync(path.join(dir, 'package.json'), JSON.stringify(pkg)); +} + +afterEach(() => { + if (tempDir) { + rmSync(tempDir, { recursive: true, force: true }); + } +}); + +describe('detectFormatter', () => { + it('returns null when no formatter is configured', () => { + const dir = createTempDir(); + writePkg(dir, { devDependencies: { typescript: '^5' } }); + + expect(detectFormatter(dir)).toBeNull(); + }); + + it('detects Prettier from a config file', () => { + const dir = createTempDir(); + writeFileSync(path.join(dir, 'prettier.config.mjs'), 'export default {};'); + + const result = detectFormatter(dir); + expect(result?.name).toBe('Prettier'); + expect(result?.bin).toBe('prettier'); + expect(result?.writeArgs).toEqual(['--write']); + }); + + it('detects Prettier from the "prettier" key in package.json', () => { + const dir = createTempDir(); + writePkg(dir, { prettier: { singleQuote: false } }); + + expect(detectFormatter(dir)?.name).toBe('Prettier'); + }); + + it('detects Prettier from devDependencies', () => { + const dir = createTempDir(); + writePkg(dir, { devDependencies: { prettier: '^3' } }); + + expect(detectFormatter(dir)?.name).toBe('Prettier'); + }); + + it('detects Biome from biome.json', () => { + const dir = createTempDir(); + writeFileSync(path.join(dir, 'biome.json'), '{}'); + + const result = detectFormatter(dir); + expect(result?.name).toBe('Biome'); + expect(result?.bin).toBe('biome'); + expect(result?.writeArgs).toEqual(['format', '--write']); + }); + + it('does not detect dprint — a lone dprint.json yields null', () => { + const dir = createTempDir(); + writeFileSync(path.join(dir, 'dprint.json'), '{}'); + + expect(detectFormatter(dir)).toBeNull(); + }); + + it('detects ESLint when only ESLint is configured', () => { + const dir = createTempDir(); + writeFileSync(path.join(dir, 'eslint.config.js'), 'export default [];'); + + const result = detectFormatter(dir); + expect(result?.name).toBe('ESLint'); + expect(result?.bin).toBe('eslint'); + expect(result?.writeArgs).toEqual(['--fix']); + }); + + it('prefers Prettier over ESLint when both are configured (the common prettier-plugin case)', () => { + const dir = createTempDir(); + writeFileSync(path.join(dir, 'eslint.config.js'), 'export default [];'); + writeFileSync(path.join(dir, 'prettier.config.mjs'), 'export default {};'); + + expect(detectFormatter(dir)?.name).toBe('Prettier'); + }); + + it('prefers Biome over Prettier when both are configured', () => { + const dir = createTempDir(); + writeFileSync(path.join(dir, 'biome.json'), '{}'); + writeFileSync(path.join(dir, 'prettier.config.mjs'), 'export default {};'); + + expect(detectFormatter(dir)?.name).toBe('Biome'); + }); + + it('walks up directory levels to find the formatter (monorepo layout)', () => { + const dir = createTempDir(); + const src = path.join(dir, 'packages', 'mcp', 'src'); + mkdirSync(src, { recursive: true }); + writeFileSync(path.join(dir, 'prettier.config.mjs'), 'export default {};'); + + expect(detectFormatter(src)?.name).toBe('Prettier'); + }); + + it('stops at the .git boundary and does not detect config above the repo root', () => { + const dir = createTempDir(); + const src = path.join(dir, 'project', 'src'); + mkdirSync(src, { recursive: true }); + mkdirSync(path.join(dir, 'project', '.git'), { recursive: true }); + // Config lives above the repo root — must not be picked up. + writeFileSync(path.join(dir, 'prettier.config.mjs'), 'export default {};'); + + expect(detectFormatter(src)).toBeNull(); + }); +}); From ab4b65856e10819afdeb25d7d0d2855685984c88 Mon Sep 17 00:00:00 2001 From: Konstantin Konstantinov Date: Tue, 23 Jun 2026 15:25:49 +0300 Subject: [PATCH 02/21] fix(codemod): match extensionless SDK subpath import specifiers IMPORT_MAP was looked up by exact key, so extensionless specifiers like @modelcontextprotocol/sdk/types (vs .../types.js) fell through to an 'Unknown SDK import path' diagnostic and were left unmigrated. Add a shared lookupImportMapping() that tolerates .js/.mjs/.cjs extension variance, and use it for import, re-export, and mock-path resolution. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_016f6h88mdVxLUdx1cNT96pW --- .../migrations/v1-to-v2/mappings/importMap.ts | 25 +++++++++++++++++++ .../v1-to-v2/transforms/importPaths.ts | 6 ++--- .../v1-to-v2/transforms/mockPaths.ts | 4 +-- .../v1-to-v2/transforms/importPaths.test.ts | 23 +++++++++++++++++ .../v1-to-v2/transforms/mockPaths.test.ts | 13 ++++++++++ 5 files changed, 66 insertions(+), 5 deletions(-) diff --git a/packages/codemod/src/migrations/v1-to-v2/mappings/importMap.ts b/packages/codemod/src/migrations/v1-to-v2/mappings/importMap.ts index 24d086f8de..bf229772b0 100644 --- a/packages/codemod/src/migrations/v1-to-v2/mappings/importMap.ts +++ b/packages/codemod/src/migrations/v1-to-v2/mappings/importMap.ts @@ -190,3 +190,28 @@ for (const barrelSpecifier of ['@modelcontextprotocol/sdk/validation/index.js', export function isAuthImport(specifier: string): boolean { return specifier.includes('/server/auth/') || specifier.includes('/server/auth.'); } + +// SDK subpath specifiers can be written with or without a JS extension +// (e.g. `@modelcontextprotocol/sdk/types` vs `.../types.js`) depending on the +// consumer's module resolution (`bundler`/`nodenext` allow the extensionless form). +// Normalize the extension so both spellings resolve to the same mapping. Built +// after every IMPORT_MAP entry above is populated; entries whose `.js` and +// extensionless forms coexist (e.g. `experimental/tasks`) share an identical +// mapping, so the collapse is lossless. +function stripJsExtension(specifier: string): string { + return specifier.replace(/\.(?:js|mjs|cjs)$/, ''); +} + +const NORMALIZED_IMPORT_MAP: Record = {}; +for (const [key, mapping] of Object.entries(IMPORT_MAP)) { + NORMALIZED_IMPORT_MAP[stripJsExtension(key)] = mapping; +} + +/** + * Resolves the v2 mapping for a v1 SDK import/export/mock specifier, tolerating + * JS extension variance. An exact match always wins; otherwise the specifier is + * matched ignoring a trailing `.js`/`.mjs`/`.cjs` (or its absence). + */ +export function lookupImportMapping(specifier: string): ImportMapping | undefined { + return IMPORT_MAP[specifier] ?? NORMALIZED_IMPORT_MAP[stripJsExtension(specifier)]; +} diff --git a/packages/codemod/src/migrations/v1-to-v2/transforms/importPaths.ts b/packages/codemod/src/migrations/v1-to-v2/transforms/importPaths.ts index 482ae3e57d..14c63f9233 100644 --- a/packages/codemod/src/migrations/v1-to-v2/transforms/importPaths.ts +++ b/packages/codemod/src/migrations/v1-to-v2/transforms/importPaths.ts @@ -5,7 +5,7 @@ import { renameAllReferences } from '../../../utils/astUtils.js'; import { actionRequired, info, v2Gap, warning } from '../../../utils/diagnostics.js'; import { addOrMergeImport, getSdkExports, getSdkImports, isTypeOnlyImport } from '../../../utils/importUtils.js'; import { resolveTypesPackage } from '../../../utils/projectAnalyzer.js'; -import { IMPORT_MAP, isAuthImport } from '../mappings/importMap.js'; +import { isAuthImport, lookupImportMapping } from '../mappings/importMap.js'; import { SIMPLE_RENAMES } from '../mappings/symbolMap.js'; const REEXPORT_WARNINGS: Record = { @@ -71,7 +71,7 @@ export const importPathsTransform: Transform = { const defaultImport = imp.getDefaultImport(); const namespaceImport = imp.getNamespaceImport(); - let mapping = IMPORT_MAP[specifier]; + let mapping = lookupImportMapping(specifier); if (!mapping && isAuthImport(specifier)) { mapping = { @@ -223,7 +223,7 @@ function rewriteExportDeclarations( if (!specifier) continue; const line = exp.getStartLineNumber(); - let mapping = IMPORT_MAP[specifier]; + let mapping = lookupImportMapping(specifier); if (!mapping && isAuthImport(specifier)) { mapping = { diff --git a/packages/codemod/src/migrations/v1-to-v2/transforms/mockPaths.ts b/packages/codemod/src/migrations/v1-to-v2/transforms/mockPaths.ts index 65ce7a4d6b..c80c695944 100644 --- a/packages/codemod/src/migrations/v1-to-v2/transforms/mockPaths.ts +++ b/packages/codemod/src/migrations/v1-to-v2/transforms/mockPaths.ts @@ -5,7 +5,7 @@ import type { Diagnostic, Transform, TransformContext, TransformResult } from '. import { actionRequired, v2Gap, warning } from '../../../utils/diagnostics.js'; import { isSdkSpecifier } from '../../../utils/importUtils.js'; import { resolveTypesPackage } from '../../../utils/projectAnalyzer.js'; -import { IMPORT_MAP, isAuthImport } from '../mappings/importMap.js'; +import { isAuthImport, lookupImportMapping } from '../mappings/importMap.js'; import { SIMPLE_RENAMES } from '../mappings/symbolMap.js'; const MOCK_METHODS = new Set([ @@ -58,7 +58,7 @@ function resolveTarget( | { target: string; renamedSymbols?: Record; symbolTargetOverrides?: Record } | { removed: true; isV2Gap?: boolean; removalMessage?: string } | null { - const mapping = IMPORT_MAP[specifier]; + const mapping = lookupImportMapping(specifier); if (!mapping && isAuthImport(specifier)) return { target: '@modelcontextprotocol/server-legacy/auth' }; if (!mapping) return null; if (mapping.status === 'removed') return { removed: true, isV2Gap: mapping.isV2Gap, removalMessage: mapping.removalMessage }; diff --git a/packages/codemod/test/v1-to-v2/transforms/importPaths.test.ts b/packages/codemod/test/v1-to-v2/transforms/importPaths.test.ts index f0563f9f69..8a63497cac 100644 --- a/packages/codemod/test/v1-to-v2/transforms/importPaths.test.ts +++ b/packages/codemod/test/v1-to-v2/transforms/importPaths.test.ts @@ -95,6 +95,18 @@ describe('import-paths transform', () => { expect(result).toContain(`from "@modelcontextprotocol/server"`); }); + it('resolves extensionless sdk/types (no .js suffix) the same as sdk/types.js', () => { + const input = `import { CallToolResult } from '@modelcontextprotocol/sdk/types';\n`; + const project = new Project({ useInMemoryFileSystem: true }); + const sourceFile = project.createSourceFile('test.ts', input); + const result = importPathsTransform.apply(sourceFile, { projectType: 'server' }); + const output = sourceFile.getFullText(); + expect(output).toContain(`from "@modelcontextprotocol/server"`); + expect(output).toContain('CallToolResult'); + expect(output).not.toContain('@modelcontextprotocol/sdk'); + expect(result.diagnostics.map(d => d.message).join('\n')).not.toContain('Unknown SDK import path'); + }); + it('preserves type-only imports separately', () => { const input = [ `import { Client } from '@modelcontextprotocol/sdk/client/index.js';`, @@ -339,6 +351,17 @@ describe('import-paths transform', () => { expect(result.diagnostics.some(d => d.message.includes('SSEServerTransport is deprecated'))).toBe(true); }); + it('resolves extensionless sdk/types re-export (no .js suffix)', () => { + const input = `export { CallToolResult } from '@modelcontextprotocol/sdk/types';\n`; + const project = new Project({ useInMemoryFileSystem: true }); + const sourceFile = project.createSourceFile('test.ts', input); + const result = importPathsTransform.apply(sourceFile, { projectType: 'server' }); + const output = sourceFile.getFullText(); + expect(output).toContain('@modelcontextprotocol/server'); + expect(output).not.toContain('@modelcontextprotocol/sdk'); + expect(result.diagnostics.map(d => d.message).join('\n')).not.toContain('Unknown SDK export path'); + }); + it('includes server-legacy in usedPackages for SSE import', () => { const project = new Project({ useInMemoryFileSystem: true }); const sourceFile = project.createSourceFile( diff --git a/packages/codemod/test/v1-to-v2/transforms/mockPaths.test.ts b/packages/codemod/test/v1-to-v2/transforms/mockPaths.test.ts index 2922618986..5fa76adfbf 100644 --- a/packages/codemod/test/v1-to-v2/transforms/mockPaths.test.ts +++ b/packages/codemod/test/v1-to-v2/transforms/mockPaths.test.ts @@ -63,6 +63,19 @@ describe('mock-paths transform', () => { expect(result).toContain(`'@modelcontextprotocol/server'`); expect(result).not.toContain('@modelcontextprotocol/sdk'); }); + + it('rewrites extensionless sdk/types path (no .js suffix)', () => { + const input = [ + `vi.doMock('@modelcontextprotocol/sdk/types', async importOriginal => {`, + ` const original = await importOriginal();`, + ` return { ...original, isInitializeRequest: mockFn };`, + `});`, + '' + ].join('\n'); + const result = applyTransform(input); + expect(result).toContain(`'@modelcontextprotocol/server'`); + expect(result).not.toContain('@modelcontextprotocol/sdk'); + }); }); describe('vi.mock', () => { From 7097be9e2a58dfeedc2a3393fc9d5696c1098fed Mon Sep 17 00:00:00 2001 From: Konstantin Konstantinov Date: Wed, 24 Jun 2026 10:19:00 +0300 Subject: [PATCH 03/21] add canonical zod schema exports from @modelcontextprotocol/sdk-shared, add codemod --- .changeset/add-sdk-shared-package.md | 5 + .changeset/codemod-sdk-shared-routing.md | 5 + .changeset/pre.json | 1 + docs/migration-SKILL.md | 5 +- docs/migration.md | 24 +- ...5-05-21-codemod-findreferences-refactor.md | 957 ++++++++++++++++++ .../2026-05-15-codemod-batch-test-fixes.md | 549 ++++++++++ .../plans/2026-06-02-readbuffer-max-size.md | 323 ++++++ .../2026-06-02-v1-readbuffer-max-size.md | 356 +++++++ .../plans/2026-06-23-sdk-shared-package.md | 716 +++++++++++++ .../2026-05-11-codemod-batch-test-design.md | 288 ++++++ .../2026-06-02-readbuffer-max-size-design.md | 61 ++ .../specs/2026-06-08-sep-2549-ttl-design.md | 495 +++++++++ .../2026-06-23-sdk-shared-package-design.md | 135 +++ packages/codemod/batch-test/repos.json | 16 +- packages/codemod/package.json | 3 +- .../codemod/scripts/generateSpecSchemaMap.ts | 39 - packages/codemod/scripts/generateVersions.ts | 3 +- packages/codemod/src/bin/batchTest.ts | 1 + .../codemod/src/generated/specSchemaMap.ts | 173 ---- packages/codemod/src/generated/versions.ts | 3 +- .../migrations/v1-to-v2/mappings/importMap.ts | 8 + .../v1-to-v2/transforms/importPaths.ts | 66 +- .../migrations/v1-to-v2/transforms/index.ts | 8 +- .../v1-to-v2/transforms/specSchemaAccess.ts | 392 ------- packages/codemod/src/utils/importUtils.ts | 1 + .../codemod/test/commentInsertion.test.ts | 67 +- .../v1-to-v2/transforms/importPaths.test.ts | 83 +- .../transforms/specSchemaAccess.test.ts | 517 ---------- packages/sdk-shared/README.md | 34 + packages/sdk-shared/eslint.config.mjs | 12 + packages/sdk-shared/package.json | 63 ++ packages/sdk-shared/src/index.ts | 177 ++++ .../sdk-shared/test/sdkSharedSchemas.test.ts | 28 + packages/sdk-shared/tsconfig.json | 11 + packages/sdk-shared/tsdown.config.ts | 28 + packages/sdk-shared/typedoc.json | 4 + packages/sdk-shared/vitest.config.js | 3 + pnpm-lock.yaml | 62 +- typedoc.config.mjs | 14 +- 40 files changed, 4532 insertions(+), 1204 deletions(-) create mode 100644 .changeset/add-sdk-shared-package.md create mode 100644 .changeset/codemod-sdk-shared-routing.md create mode 100644 docs/superpowers/plans/2025-05-21-codemod-findreferences-refactor.md create mode 100644 docs/superpowers/plans/2026-05-15-codemod-batch-test-fixes.md create mode 100644 docs/superpowers/plans/2026-06-02-readbuffer-max-size.md create mode 100644 docs/superpowers/plans/2026-06-02-v1-readbuffer-max-size.md create mode 100644 docs/superpowers/plans/2026-06-23-sdk-shared-package.md create mode 100644 docs/superpowers/specs/2026-05-11-codemod-batch-test-design.md create mode 100644 docs/superpowers/specs/2026-06-02-readbuffer-max-size-design.md create mode 100644 docs/superpowers/specs/2026-06-08-sep-2549-ttl-design.md create mode 100644 docs/superpowers/specs/2026-06-23-sdk-shared-package-design.md delete mode 100644 packages/codemod/scripts/generateSpecSchemaMap.ts delete mode 100644 packages/codemod/src/generated/specSchemaMap.ts delete mode 100644 packages/codemod/src/migrations/v1-to-v2/transforms/specSchemaAccess.ts delete mode 100644 packages/codemod/test/v1-to-v2/transforms/specSchemaAccess.test.ts create mode 100644 packages/sdk-shared/README.md create mode 100644 packages/sdk-shared/eslint.config.mjs create mode 100644 packages/sdk-shared/package.json create mode 100644 packages/sdk-shared/src/index.ts create mode 100644 packages/sdk-shared/test/sdkSharedSchemas.test.ts create mode 100644 packages/sdk-shared/tsconfig.json create mode 100644 packages/sdk-shared/tsdown.config.ts create mode 100644 packages/sdk-shared/typedoc.json create mode 100644 packages/sdk-shared/vitest.config.js diff --git a/.changeset/add-sdk-shared-package.md b/.changeset/add-sdk-shared-package.md new file mode 100644 index 0000000000..48cc1291e1 --- /dev/null +++ b/.changeset/add-sdk-shared-package.md @@ -0,0 +1,5 @@ +--- +'@modelcontextprotocol/sdk-shared': minor +--- + +Add `@modelcontextprotocol/sdk-shared`: the public home for the MCP specification Zod schemas. It bundles the SDK's internal schema definitions and re-exports only the `*Schema` values, so consumers can validate protocol payloads (`Schema.parse(value)` / `.safeParse(value)`) without depending on a package's internal barrel. Spec types, error classes, enums, and guards continue to live on `@modelcontextprotocol/server` and `@modelcontextprotocol/client`. diff --git a/.changeset/codemod-sdk-shared-routing.md b/.changeset/codemod-sdk-shared-routing.md new file mode 100644 index 0000000000..9b9dddfbaf --- /dev/null +++ b/.changeset/codemod-sdk-shared-routing.md @@ -0,0 +1,5 @@ +--- +'@modelcontextprotocol/codemod': minor +--- + +Route v1 `@modelcontextprotocol/sdk/types.js` schema imports to the new `@modelcontextprotocol/sdk-shared` package. The `*Schema` Zod constants now migrate as a behavior-preserving import-path swap — `Schema.parse(value)` / `.safeParse(value)` keep working — while spec types, error classes, enums, and guards continue to resolve to `@modelcontextprotocol/client` / `@modelcontextprotocol/server` by context. A single `import { CallToolResult, CallToolResultSchema } from '.../types.js'` is split accordingly. The previous `specSchemaAccess` transform (which rewrote `.parse()` into `specTypeSchemas.X['~standard'].validate(...)`) is removed. diff --git a/.changeset/pre.json b/.changeset/pre.json index c4c3cf31a8..9f29aaaee2 100644 --- a/.changeset/pre.json +++ b/.changeset/pre.json @@ -18,6 +18,7 @@ "@modelcontextprotocol/node": "2.0.0-alpha.0", "@modelcontextprotocol/server": "2.0.0-alpha.0", "@modelcontextprotocol/server-legacy": "2.0.0-alpha.0", + "@modelcontextprotocol/sdk-shared": "2.0.0-alpha.0", "@modelcontextprotocol/codemod": "2.0.0-alpha.0", "@modelcontextprotocol/test-conformance": "2.0.0-alpha.0", "@modelcontextprotocol/test-helpers": "2.0.0-alpha.0", diff --git a/docs/migration-SKILL.md b/docs/migration-SKILL.md index 8c32258e3c..d61fb1a344 100644 --- a/docs/migration-SKILL.md +++ b/docs/migration-SKILL.md @@ -61,7 +61,7 @@ Replace all `@modelcontextprotocol/sdk/...` imports using this table. | v1 import path | v2 package | | ------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `@modelcontextprotocol/sdk/types.js` | `@modelcontextprotocol/client` or `@modelcontextprotocol/server` | +| `@modelcontextprotocol/sdk/types.js` | Types / error classes / enums / guards → `@modelcontextprotocol/client` or `@modelcontextprotocol/server`; Zod `*Schema` constants → `@modelcontextprotocol/sdk-shared` | | `@modelcontextprotocol/sdk/shared/protocol.js` | `@modelcontextprotocol/client` or `@modelcontextprotocol/server` | | `@modelcontextprotocol/sdk/shared/transport.js` | `@modelcontextprotocol/client` or `@modelcontextprotocol/server` | | `@modelcontextprotocol/sdk/shared/uriTemplate.js` | `@modelcontextprotocol/client` or `@modelcontextprotocol/server` | @@ -98,8 +98,7 @@ Notes: | `StreamableHTTPError` | REMOVED (use `SdkHttpError` with `SdkErrorCode.ClientHttp*`) | | `WebSocketClientTransport` | REMOVED (use `StreamableHTTPClientTransport` or `StdioClientTransport`) | -All other **type** symbols from `@modelcontextprotocol/sdk/types.js` retain their original names. **Zod schemas** (e.g., `CallToolResultSchema`, `ListToolsResultSchema`) are no longer part of the public API — they are internal to the SDK. For runtime validation, use -`isSpecType.TypeName(value)` (e.g., `isSpecType.CallToolResult(v)`) or `specTypeSchemas.TypeName` for the `StandardSchemaV1Sync` validator object. The keys are typed as `SpecTypeName`, a literal union of all spec type names. +All other **type** symbols from `@modelcontextprotocol/sdk/types.js` retain their original names — import them from `@modelcontextprotocol/client` or `@modelcontextprotocol/server`. The **Zod schemas** (e.g., `CallToolResultSchema`, `ListToolsResultSchema`) move to `@modelcontextprotocol/sdk-shared`; `Schema.parse(value)` / `.safeParse(value)` keep working unchanged (the codemod rewrites the import path). To validate **without** depending on Zod, use `isSpecType.TypeName(value)` (e.g., `isSpecType.CallToolResult(v)`) or `specTypeSchemas.TypeName` (a `StandardSchemaV1Sync` validator) from `@modelcontextprotocol/client` / `@modelcontextprotocol/server`; the keys are typed as `SpecTypeName`, a literal union of all spec type names. ### Error class changes diff --git a/docs/migration.md b/docs/migration.md index be0dbae0dd..8950d9150e 100644 --- a/docs/migration.md +++ b/docs/migration.md @@ -516,29 +516,39 @@ The return type is now inferred from the method name via `ResultTypeMap`. For ex For **custom (non-spec)** methods, keep the result-schema argument — see [Sending custom-method requests](#sending-custom-method-requests). Only drop the schema when calling a spec method. -If you were using `CallToolResultSchema` (or any `*Schema` constant) for **runtime validation** (not just in `request()`/`callTool()` calls), use `isSpecType` or `specTypeSchemas`: +If you were using `CallToolResultSchema` (or any `*Schema` constant) for **runtime validation** (not just in `request()`/`callTool()` calls), import the schema from `@modelcontextprotocol/sdk-shared`. Your `.parse()` / `.safeParse()` calls keep working unchanged — only the import path changes: ```typescript -// v1: runtime validation with Zod schema +// v1 import { CallToolResultSchema } from '@modelcontextprotocol/sdk/types.js'; if (CallToolResultSchema.safeParse(value).success) { /* ... */ } -// v2: keyed type predicate +// v2 — same code, new import path +import { CallToolResultSchema } from '@modelcontextprotocol/sdk-shared'; +if (CallToolResultSchema.safeParse(value).success) { + /* ... */ +} +``` + +`@modelcontextprotocol/sdk-shared` is the canonical home for the spec Zod schemas. `@modelcontextprotocol/server` and `@modelcontextprotocol/client` keep a Zod-free public surface, so the raw `*Schema` constants live in `sdk-shared`. (The codemod rewrites these imports for you.) + +If you'd rather **not** depend on Zod, `@modelcontextprotocol/client` and `@modelcontextprotocol/server` also expose Zod-free validators keyed by `SpecTypeName` — a literal union of every named spec type, so you get autocomplete and a compile error on typos: + +```typescript import { isSpecType } from '@modelcontextprotocol/client'; if (isSpecType.CallToolResult(value)) { /* ... */ } const blocks = mixed.filter(isSpecType.ContentBlock); -// v2: or get the StandardSchemaV1Sync validator object directly +// or the StandardSchemaV1Sync validator object directly import { specTypeSchemas } from '@modelcontextprotocol/client'; const result = specTypeSchemas.CallToolResult['~standard'].validate(value); ``` -`isSpecType` and `specTypeSchemas` are keyed by `SpecTypeName` — a literal union of every named type in the MCP spec — so you get autocomplete and a compile error on typos. `specTypeSchemas.X` is a `StandardSchemaV1Sync` — `validate()` returns the result synchronously, -so you can access `.issues` / `.value` without `await`. It composes with any Standard-Schema-aware library. The pre-existing `isCallToolResult(value)` guard still works. +`specTypeSchemas.X` is a `StandardSchemaV1Sync` — `validate()` returns the result synchronously, so you can access `.issues` / `.value` without `await`. It composes with any Standard-Schema-aware library. The pre-existing `isCallToolResult(value)` guard still works. ### Client list methods return empty results for missing capabilities @@ -584,7 +594,7 @@ The following deprecated type aliases have been removed from `@modelcontextproto | `IsomorphicHeaders` | Use Web Standard `Headers` | | `AuthInfo` (from `server/auth/types.js`) | `AuthInfo` (now re-exported by `@modelcontextprotocol/client` and `@modelcontextprotocol/server`) | -All other types and schemas exported from `@modelcontextprotocol/sdk/types.js` retain their original names — import them from `@modelcontextprotocol/client` or `@modelcontextprotocol/server`. +All other symbols exported from `@modelcontextprotocol/sdk/types.js` retain their original names. Import the **types**, error classes, enums, and guards from `@modelcontextprotocol/client` or `@modelcontextprotocol/server`, and the **Zod schemas** (the `*Schema` constants) from `@modelcontextprotocol/sdk-shared`. > **Note on `isJSONRPCResponse`:** v1's `isJSONRPCResponse` was a deprecated alias that only checked for _result_ responses (it was equivalent to `isJSONRPCResultResponse`). v2 removes the deprecated alias and introduces a **new** `isJSONRPCResponse` with corrected semantics — it > checks for _any_ response (either result or error). If you are migrating v1 code that used `isJSONRPCResponse`, rename it to `isJSONRPCResultResponse` to preserve the original behavior. Use the new `isJSONRPCResponse` only when you want to match both result and error responses. diff --git a/docs/superpowers/plans/2025-05-21-codemod-findreferences-refactor.md b/docs/superpowers/plans/2025-05-21-codemod-findreferences-refactor.md new file mode 100644 index 0000000000..d907040b24 --- /dev/null +++ b/docs/superpowers/plans/2025-05-21-codemod-findreferences-refactor.md @@ -0,0 +1,957 @@ +# Codemod: Replace Manual AST Walking with `findReferencesAsNodes()` + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Simplify codemod transforms by replacing manual `forEachDescendant` + parent-kind-guard patterns with ts-morph's `findReferencesAsNodes()`, eliminating ~12 parent-kind guards, ~4 duplicate scope checks, and ~5 manual AST walk functions. + +**Architecture:** ts-morph's TypeScript language service already resolves symbol bindings in the current syntax-only Project mode (no tsconfig needed). `findReferencesAsNodes()` returns precisely the references to a given symbol — correctly scoped, excluding property-name positions, and handling aliases. We refactor transforms to collect references via this API *before* mutating the AST, then apply changes in reverse-position order (a pattern the codemod already uses). A second phase optionally loads the user's tsconfig for receiver-type checking. + +**Tech Stack:** ts-morph v28, vitest + +**Key invariant:** `findReferencesAsNodes()` must be called *before* the symbol binding is modified (e.g., before an import specifier is renamed or removed). After mutation, collected Node objects remain valid but the language service can no longer resolve the original binding. + +--- + +## File Map + +| File | Action | Responsibility | +|------|--------|----------------| +| `packages/codemod/src/utils/astUtils.ts` | Modify | Replace `renameAllReferences` internals with `findReferencesAsNodes()` | +| `packages/codemod/src/utils/importUtils.ts` | Modify | Add `findImportSpecifierByName`, simplify `removeUnusedImport` | +| `packages/codemod/src/migrations/v1-to-v2/transforms/symbolRenames.ts` | Modify | Collect refs before import mutation; use `findReferencesAsNodes()` in ErrorCode/RHE handlers | +| `packages/codemod/src/migrations/v1-to-v2/transforms/contextTypes.ts` | Modify | Use `findReferencesAsNodes()` on `extra` param; eliminate parent-kind guards and manual scope check | +| `packages/codemod/src/migrations/v1-to-v2/transforms/specSchemaAccess.ts` | Modify | Use `findReferencesAsNodes()` for schema refs; eliminate `findNonImportReferences()` | +| `packages/codemod/src/migrations/v1-to-v2/transforms/importPaths.ts` | Modify | Collect refs before import removal for renamed symbols | +| `packages/codemod/src/migrations/v1-to-v2/transforms/removedApis.ts` | Modify | Collect refs before import removal | +| `packages/codemod/src/types.ts` | Modify | Add optional `project` to `TransformContext` (Phase 2) | +| `packages/codemod/src/runner.ts` | Modify | Optionally resolve tsconfig; pass Project via context (Phase 2) | +| `packages/codemod/src/utils/projectAnalyzer.ts` | Modify | Add `findTsConfig()` (Phase 2) | +| All test files under `packages/codemod/test/v1-to-v2/transforms/` | Verify | Existing tests must pass unchanged — this is a refactor under green | + +--- + +## Phase 1: `findReferencesAsNodes()` Refactor (no tsconfig needed) + +### Task 1: Rewrite `renameAllReferences` in astUtils.ts + +The current function (33 lines, 12 parent-kind guards) manually walks all identifiers and filters by parent kind. `findReferencesAsNodes()` eliminates 10 of those 12 guards — only `ShorthandPropertyAssignment` and `ExportSpecifier` need special handling since they require AST expansion (not just text replacement). + +**Files:** +- Modify: `packages/codemod/src/utils/astUtils.ts` +- Verify: `packages/codemod/test/v1-to-v2/transforms/symbolRenames.test.ts` (primary consumer) + +- [ ] **Step 1: Read the current implementation** + +Read `packages/codemod/src/utils/astUtils.ts` — the entire file is the `renameAllReferences` function. + +Current implementation walks all identifiers with matching text and checks 12 parent kinds: +``` +ImportSpecifier, ExportSpecifier, PropertyAssignment (name), PropertyAccessExpression (name), +PropertySignature (name), MethodDeclaration (name), MethodSignature (name), +PropertyDeclaration (name), EnumMember (name), BindingElement (propertyName), +GetAccessorDeclaration (name), SetAccessorDeclaration (name), ShorthandPropertyAssignment +``` + +- [ ] **Step 2: Rewrite using `findReferencesAsNodes()`** + +Replace the body of `renameAllReferences` with: + +```typescript +import type { SourceFile } from 'ts-morph'; +import { Node } from 'ts-morph'; + +export function renameAllReferences(sourceFile: SourceFile, oldName: string, newName: string): void { + // Find the first identifier with this name to use as the findReferences anchor. + // Must be called BEFORE the symbol's import specifier is renamed/removed. + let anchor: import('ts-morph').Node | undefined; + sourceFile.forEachDescendant(node => { + if (anchor) return; + if (Node.isIdentifier(node) && node.getText() === oldName) { + anchor = node; + } + }); + if (!anchor) return; + + const refs = anchor.findReferencesAsNodes(); + + // Apply in reverse position order to avoid invalidating earlier nodes + const sorted = refs.toSorted((a, b) => b.getStart() - a.getStart()); + for (const ref of sorted) { + if (ref.wasForgotten()) continue; + const parent = ref.getParent(); + if (!parent) continue; + + // Skip import specifiers — caller manages those + if (Node.isImportSpecifier(parent)) continue; + + // ExportSpecifier: preserve public name by adding alias + if (Node.isExportSpecifier(parent)) { + if (parent.getAliasNode() === ref) continue; + if (!parent.getAliasNode()) parent.setAlias(oldName); + parent.getNameNode().replaceWithText(newName); + continue; + } + + // ShorthandPropertyAssignment: expand { McpError } → { McpError: ProtocolError } + if (Node.isShorthandPropertyAssignment(parent)) { + parent.replaceWithText(`${oldName}: ${newName}`); + continue; + } + + ref.replaceWithText(newName); + } +} +``` + +The 10 parent-kind guards (PropertyAssignment name, PropertyAccessExpression name, PropertySignature name, MethodDeclaration name, MethodSignature name, PropertyDeclaration name, EnumMember name, BindingElement propertyName, GetAccessor name, SetAccessor name) are all handled automatically by `findReferencesAsNodes()` — it never returns identifier nodes in property-name positions. + +- [ ] **Step 3: Run all transform tests to verify** + +Run: `pnpm --filter @modelcontextprotocol/codemod test` + +Expected: all tests pass. The `renameAllReferences` function is called by `symbolRenames`, `importPaths`, and `removedApis` transforms — all their tests exercise it. + +- [ ] **Step 4: Suggest commit** + +``` +feat(codemod): rewrite renameAllReferences using findReferencesAsNodes + +Replace manual 12-case parent-kind guard with ts-morph's +findReferencesAsNodes() which handles scope and position +classification automatically. Only ShorthandPropertyAssignment +and ExportSpecifier need explicit handling for AST expansion. +``` + +--- + +### Task 2: Refactor `symbolRenames.ts` — collect refs before import mutation + +The SIMPLE_RENAMES loop currently modifies the import specifier first, then calls `renameAllReferences`. But `findReferencesAsNodes()` must be called *before* the binding is modified. This task reorders the operations. + +The three `forEachDescendant` walks in `handleErrorCodeSplit` and `handleRequestHandlerExtra` are also replaced. + +**Files:** +- Modify: `packages/codemod/src/migrations/v1-to-v2/transforms/symbolRenames.ts` +- Verify: `packages/codemod/test/v1-to-v2/transforms/symbolRenames.test.ts` + +- [ ] **Step 1: Read current file** + +Read `packages/codemod/src/migrations/v1-to-v2/transforms/symbolRenames.ts` (352 lines). + +The SIMPLE_RENAMES loop (lines 23-37): +```typescript +for (const namedImport of imp.getNamedImports()) { + const name = namedImport.getName(); + const newName = SIMPLE_RENAMES[name]; + if (newName) { + namedImport.setName(newName); // modifies binding FIRST + const alias = namedImport.getAliasNode(); + if (!alias) { + renameAllReferences(sourceFile, name, newName); // then renames body + } + changesCount++; + } +} +``` + +- [ ] **Step 2: Reorder SIMPLE_RENAMES to collect-before-mutate** + +```typescript +for (const namedImport of imp.getNamedImports()) { + const name = namedImport.getName(); + const newName = SIMPLE_RENAMES[name]; + if (newName) { + const alias = namedImport.getAliasNode(); + if (!alias) { + // Collect refs while binding is still intact + renameAllReferences(sourceFile, name, newName); + } + namedImport.setName(newName); // modify binding AFTER refs are renamed + changesCount++; + } +} +``` + +Note: this is just reordering the two operations. `renameAllReferences` (from Task 1) now uses `findReferencesAsNodes()` internally, which requires the binding to still exist. Moving `setName` after `renameAllReferences` satisfies this requirement. + +- [ ] **Step 3: Refactor `handleErrorCodeSplit` to use `findReferencesAsNodes()`** + +Current code (lines 71-85) does a manual `forEachDescendant` looking for `Node.isPropertyAccessExpression` where the expression is `ErrorCode`. Replace with: + +```typescript +function handleErrorCodeSplit(sourceFile: SourceFile, diagnostics: Diagnostic[]): number { + let changesCount = 0; + + const imports = sourceFile.getImportDeclarations(); + let errorCodeImport: ReturnType<(typeof imports)[0]['getNamedImports']>[0] | undefined; + + for (const imp of imports) { + if (!isAnyMcpSpecifier(imp.getModuleSpecifierValue())) continue; + for (const namedImport of imp.getNamedImports()) { + if (namedImport.getName() === 'ErrorCode') { + errorCodeImport = namedImport; + break; + } + } + if (errorCodeImport) break; + } + + if (!errorCodeImport) return 0; + + // Collect ALL references while binding exists + const refs = errorCodeImport.getNameNode().findReferencesAsNodes() + .filter(n => !Node.isImportSpecifier(n.getParent())); + + let needsProtocolErrorCode = false; + let needsSdkErrorCode = false; + + // Classify each reference + const replacements: { node: import('ts-morph').Node; newText: string }[] = []; + for (const ref of refs) { + const parent = ref.getParent(); + if (!parent || !Node.isPropertyAccessExpression(parent)) continue; + if (parent.getExpression() !== ref) continue; + + const member = parent.getName(); + if (ERROR_CODE_SDK_MEMBERS.has(member)) { + needsSdkErrorCode = true; + replacements.push({ node: ref, newText: 'SdkErrorCode' }); + } else { + needsProtocolErrorCode = true; + replacements.push({ node: ref, newText: 'ProtocolErrorCode' }); + } + changesCount++; + } + + // Apply replacements in reverse order + const sorted = replacements.toSorted((a, b) => b.node.getStart() - a.node.getStart()); + for (const { node, newText } of sorted) { + node.replaceWithText(newText); + } + + // ... rest of import cleanup (unchanged from current code, lines 87-143) ... +``` + +This eliminates the `forEachDescendant` walk. The `errorCodeLocalName` variable and manual alias handling are also gone — `findReferencesAsNodes()` resolves aliases automatically. + +- [ ] **Step 4: Refactor `handleRequestHandlerExtra` similarly** + +The `forEachDescendant` walk at line 189 that finds `Node.isTypeReference` matching `extraLocalName` becomes: + +```typescript +// Collect refs while binding exists +const refs = extraImport.getNameNode().findReferencesAsNodes() + .filter(n => !Node.isImportSpecifier(n.getParent())); +``` + +The rest of the classification logic (checking `ServerRequest`/`ClientNotification` type args) stays the same — it operates on the parent `TypeReference` node. But we no longer need `extraLocalName` or manual alias handling. + +- [ ] **Step 5: Run tests** + +Run: `pnpm --filter @modelcontextprotocol/codemod test -- test/v1-to-v2/transforms/symbolRenames.test.ts` + +Expected: all tests pass, including alias tests (lines 366-399). + +- [ ] **Step 6: Suggest commit** + +``` +refactor(codemod): use findReferencesAsNodes in symbolRenames + +Collect symbol references via findReferencesAsNodes() before +mutating import specifiers. Eliminates three forEachDescendant +walks and manual alias tracking in handleErrorCodeSplit and +handleRequestHandlerExtra. +``` + +--- + +### Task 3: Refactor `contextTypes.ts` — eliminate parent-kind guards and scope checks + +This transform has the second-highest complexity. The `processCallback` function (lines 18-177): +- Walks callback body with `forEachDescendant` looking for `extra` identifiers (line 98) +- Checks 4 parent kinds to exclude property-name positions (lines 102-105) +- Does a separate `forEachDescendant` walk to check for `ctx` name conflicts (lines 63-74) +- Builds replacements with property mappings (lines 111-134) + +All of this collapses with `findReferencesAsNodes()` on the parameter. + +**Files:** +- Modify: `packages/codemod/src/migrations/v1-to-v2/transforms/contextTypes.ts` +- Verify: `packages/codemod/test/v1-to-v2/transforms/contextTypes.test.ts` + +- [ ] **Step 1: Read the current processCallback function** + +Read `packages/codemod/src/migrations/v1-to-v2/transforms/contextTypes.ts:18-177`. + +Key sections to replace: +- Lines 61-84: scope-conflict check (walk looking for `ctx` identifier) +- Lines 96-107: collect identifiers matching `extra`, filter 4 parent kinds +- Lines 110-135: build replacements + +- [ ] **Step 2: Replace identifier collection with findReferencesAsNodes** + +Replace lines 62-84 (scope conflict check) and lines 96-107 (identifier collection) with: + +```typescript + // Check for ctx name conflicts in the callback body using findReferences on + // any existing 'ctx' identifier — if found, it means ctx is in scope. + if (body) { + let ctxAlreadyInScope = false; + body.forEachDescendant(node => { + if (ctxAlreadyInScope) return; + if (Node.isIdentifier(node) && node.getText() === CTX_PARAM_NAME) { + // Check it's not inside a nested function that shadows it + const containingFn = node.getFirstAncestor(n => + Node.isArrowFunction(n) || Node.isFunctionExpression(n) || Node.isFunctionDeclaration(n) + ); + if (containingFn === callbackNode || !containingFn) { + ctxAlreadyInScope = true; + } + } + }); + if (ctxAlreadyInScope) { + diagnostics.push( + warning( + sourceFile.getFilePath(), + extraParam.getStartLineNumber(), + `Cannot rename '${EXTRA_PARAM_NAME}' to '${CTX_PARAM_NAME}': '${CTX_PARAM_NAME}' is already referenced in this scope. Manual migration required.` + ) + ); + return -1; + } + } + + // Collect references to the 'extra' parameter using findReferencesAsNodes. + // This automatically: + // - scopes to this specific parameter binding (ignores shadowed 'extra' in nested fns) + // - excludes property-name positions ({ extra: value }, obj.extra, etc.) + const paramRefs = extraParam.getNameNode().findReferencesAsNodes() + .filter(n => !Node.isParameter(n.getParent())); + + // Rename param declaration + const paramDecl = extraParam.getNameNode(); + paramDecl.replaceWithText(CTX_PARAM_NAME); + + // Build replacements from collected references + const sortedMappings = [...CONTEXT_PROPERTY_MAP] + .filter(m => m.from !== m.to) + .toSorted((a, b) => b.from.length - a.from.length); + + const replacements: { node: import('ts-morph').Node; newText: string }[] = []; + for (const ref of paramRefs) { + const parent = ref.getParent(); + // Value-position property access: extra.signal → ctx.mcpReq.signal + if (parent && Node.isPropertyAccessExpression(parent) && parent.getExpression() === ref) { + const propName = '.' + parent.getName(); + const mapping = sortedMappings.find(m => m.from === propName); + if (mapping) { + replacements.push({ node: parent, newText: CTX_PARAM_NAME + mapping.to }); + continue; + } + } + // Type-position qualified name: typeof extra.signal → typeof ctx.mcpReq.signal + if (parent && parent.getKind() === SyntaxKind.QualifiedName && parent.getChildAtIndex(0) === ref) { + const right = parent.getChildAtIndex(2); + if (right) { + const propName = '.' + right.getText(); + const mapping = sortedMappings.find(m => m.from === propName); + if (mapping) { + replacements.push({ node: parent, newText: CTX_PARAM_NAME + mapping.to }); + continue; + } + } + } + replacements.push({ node: ref, newText: CTX_PARAM_NAME }); + } + + const sorted = replacements.toSorted((a, b) => b.node.getStart() - a.node.getStart()); + for (const { node, newText } of sorted) { + node.replaceWithText(newText); + } +``` + +**What's eliminated:** +- The 4-case parent-kind exclusion list (lines 102-106) — `findReferencesAsNodes()` handles these +- The nested-function-aware scope walk for conflict detection (lines 63-74) — simplified to a targeted check + +**What stays the same:** +- Property mapping logic (PropertyAccessExpression / QualifiedName) — this is transform-specific +- The outer call-finding loop and callback detection +- The post-rewrite destructuring warning + +- [ ] **Step 3: Run tests** + +Run: `pnpm --filter @modelcontextprotocol/codemod test -- test/v1-to-v2/transforms/contextTypes.test.ts` + +Expected: all tests pass. Key tests to watch: +- `should not rename 'extra' in property positions` (verifies parent-kind exclusion) +- `should not rename when ctx already exists` (verifies scope conflict) +- `should handle nested functions` (verifies scope isolation) + +- [ ] **Step 4: Suggest commit** + +``` +refactor(codemod): use findReferencesAsNodes in contextTypes + +Replace manual forEachDescendant + 4-case parent-kind guard with +findReferencesAsNodes() on the 'extra' parameter. The language +service handles scope isolation and property-name exclusion +automatically. +``` + +--- + +### Task 4: Refactor `specSchemaAccess.ts` — eliminate `findNonImportReferences` and scoped walks + +This is the most complex transform (350 lines, 6 parent-kind guards, 3-level parent walks). Two `forEachDescendant` walks are replaced. + +**Files:** +- Modify: `packages/codemod/src/migrations/v1-to-v2/transforms/specSchemaAccess.ts` +- Verify: `packages/codemod/test/v1-to-v2/transforms/specSchemaAccess.test.ts` + +- [ ] **Step 1: Read the current file** + +Read `packages/codemod/src/migrations/v1-to-v2/transforms/specSchemaAccess.ts`. + +Key sections: +- `findNonImportReferences` (lines 51-61): manual forEachDescendant walk +- `handleReference` (lines 63-192): 6 parent-kind guards at lines 129, 143, 154, 168, 172, 176 +- `rewriteCapturedSafeParse` (lines 249-335): scoped forEachDescendant walk at line 269 + +- [ ] **Step 2: Replace `findNonImportReferences` with `findReferencesAsNodes`** + +In the main loop (lines 19-31), replace: +```typescript +const refs = findNonImportReferences(sourceFile, localName); +``` +with: +```typescript +// Find the import specifier node for this schema +const specNode = schemaImports.get(localName)!.specifier; +const refs = specNode.getNameNode().findReferencesAsNodes() + .filter(n => !Node.isImportSpecifier(n.getParent())); +``` + +This requires changing `collectSpecSchemaImports` to also return the specifier node: +```typescript +function collectSpecSchemaImports(sourceFile: SourceFile): Map { + const result = new Map(); + for (const imp of sourceFile.getImportDeclarations()) { + if (!isAnyMcpSpecifier(imp.getModuleSpecifierValue())) continue; + for (const n of imp.getNamedImports()) { + const exportName = n.getName(); + if (!SPEC_SCHEMA_NAMES.has(exportName)) continue; + const localName = n.getAliasNode()?.getText() ?? exportName; + result.set(localName, { originalName: exportName, specifier: n }); + } + } + return result; +} +``` + +Delete the `findNonImportReferences` function entirely. + +- [ ] **Step 3: Simplify `handleReference` parent-kind guards** + +With `findReferencesAsNodes()`, we no longer get identifiers in property-name positions. Remove these now-unreachable guards: + +```typescript +// REMOVE — findReferencesAsNodes never returns property-name-position identifiers: +// - line 168: Node.isPropertyAssignment(parent) && parent.getNameNode() === ref +// - line 172: Node.isBindingElement(parent) && parent.getPropertyNameNode() === ref +// - line 176: Node.isPropertyAccessExpression(parent) && parent.getNameNode() === ref +``` + +Keep these — they classify the reference type, not exclude positions: +- `isTypeofInTypePosition` — distinguishes type-level `typeof X` from value usage +- `isSafeParseSuccessPattern` / `isSafeParsePattern` / `isParsePattern` — detect Zod API patterns +- `Node.isPropertyAccessExpression(parent) && parent.getExpression() === ref` — value-position property access +- `Node.isExportSpecifier(parent)` — re-export position +- `Node.isShorthandPropertyAssignment(parent)` — shorthand property + +- [ ] **Step 4: Replace scoped walk in `rewriteCapturedSafeParse`** + +Current code (lines 268-317) does `scope.forEachDescendant` to find `${varName}.success`, `${varName}.data`, `${varName}.error` accesses. Replace with `findReferencesAsNodes()` on the variable declaration: + +```typescript +function rewriteCapturedSafeParse( + safeParseCall: import('ts-morph').CallExpression, + localName: string, + typeName: string, + sourceFile: SourceFile, + diagnostics: Diagnostic[] +): boolean { + const varDecl = safeParseCall.getParent() as import('ts-morph').VariableDeclaration; + const varName = varDecl.getName(); + const args = safeParseCall.getArguments(); + const argText = args.length > 0 ? args[0]!.getText() : ''; + + // Collect references to the result variable BEFORE rewriting the initializer + const varNameNode = varDecl.getNameNode(); + const varRefs = varNameNode.findReferencesAsNodes() + .filter(n => n !== varNameNode && !Node.isVariableDeclaration(n.getParent())); + + // Rewrite the safeParse call + safeParseCall.replaceWithText(`specTypeSchemas.${typeName}['~standard'].validate(${argText})`); + ensureImport(sourceFile, 'specTypeSchemas'); + + // Classify property accesses on the result variable + const replacements: { node: import('ts-morph').Node; newText: string }[] = []; + for (const ref of varRefs) { + const parent = ref.getParent(); + if (!parent || !Node.isPropertyAccessExpression(parent)) continue; + if (parent.getExpression() !== ref) continue; + + const propName = parent.getName(); + switch (propName) { + case 'success': { + const grandParent = parent.getParent(); + if (grandParent && Node.isPrefixUnaryExpression(grandParent) && + grandParent.getOperatorToken() === SyntaxKind.ExclamationToken) { + replacements.push({ node: grandParent, newText: `${varName}.issues !== undefined` }); + } else { + replacements.push({ node: parent, newText: `(${varName}.issues === undefined)` }); + } + break; + } + case 'data': + replacements.push({ node: parent, newText: `${varName}.value` }); + break; + case 'error': { + const errorParent = parent.getParent(); + if (errorParent && Node.isPropertyAccessExpression(errorParent) && errorParent.getExpression() === parent) { + const subProp = errorParent.getName(); + if (subProp === 'issues') { + replacements.push({ node: errorParent, newText: `${varName}.issues` }); + } else if (subProp === 'message') { + replacements.push({ node: errorParent, newText: `${varName}.issues?.map(i => i.message).join(', ')` }); + } else { + diagnostics.push(warning(sourceFile.getFilePath(), errorParent.getStartLineNumber(), + `${varName}.error.${subProp} has no StandardSchema equivalent. Manual migration required.`)); + } + } else { + replacements.push({ node: parent, newText: `${varName}.issues` }); + } + break; + } + } + } + + const sorted = replacements.toSorted((a, b) => b.node.getStart() - a.node.getStart()); + for (const { node, newText } of sorted) { + node.replaceWithText(newText); + } + + diagnostics.push(warning(sourceFile.getFilePath(), varDecl.getStartLineNumber(), + `Rewrote ${localName}.safeParse() to specTypeSchemas.${typeName}['~standard'].validate(). ` + + `Result properties remapped: .success → .issues === undefined, .data → .value, .error → .issues.`)); + + return true; +} +``` + +**What's eliminated:** +- `findNonImportReferences` function (11 lines) — deleted entirely +- 3 unreachable parent-kind guards in `handleReference` +- The `scope.forEachDescendant` walk in `rewriteCapturedSafeParse` (was scope-insensitive anyway, as a PR comment noted) + +- [ ] **Step 5: Run tests** + +Run: `pnpm --filter @modelcontextprotocol/codemod test -- test/v1-to-v2/transforms/specSchemaAccess.test.ts` + +Expected: all tests pass. Key tests: +- Aliased import `import { CallToolRequestSchema as CTRS }` (line 493) +- Captured safeParse rewrite (line 248+) +- Non-MCP schemas not touched (line 222+) + +- [ ] **Step 6: Suggest commit** + +``` +refactor(codemod): use findReferencesAsNodes in specSchemaAccess + +Delete findNonImportReferences() and replace both forEachDescendant +walks with findReferencesAsNodes(). The scoped safeParse-result +rewrite now uses findReferencesAsNodes on the variable declaration, +which is inherently scope-correct. +``` + +--- + +### Task 5: Refactor `importPaths.ts` — collect refs before import removal + +Currently, `importPaths.ts` removes the old import (line 170), then calls `renameAllReferences` (line 172). Since Task 1's `renameAllReferences` now uses `findReferencesAsNodes()`, the binding must exist when it's called. Reorder operations. + +**Files:** +- Modify: `packages/codemod/src/migrations/v1-to-v2/transforms/importPaths.ts` +- Verify: `packages/codemod/test/v1-to-v2/transforms/importPaths.test.ts` + +- [ ] **Step 1: Read the relevant section** + +Read `packages/codemod/src/migrations/v1-to-v2/transforms/importPaths.ts:106-175`. + +The issue is at lines 162-175: +```typescript +for (const n of namedImports) { + // ... add pending imports ... +} +imp.remove(); // ← removes binding +changesCount++; +for (const [oldName, newName] of symbolsToRenameInFile) { + renameAllReferences(sourceFile, oldName, newName); // ← needs binding +} +``` + +- [ ] **Step 2: Move rename before import removal** + +```typescript +// Rename body references BEFORE removing the import (findReferencesAsNodes needs the binding) +for (const [oldName, newName] of symbolsToRenameInFile) { + renameAllReferences(sourceFile, oldName, newName); +} + +for (const n of namedImports) { + const name = n.getName(); + const resolvedName = mapping.renamedSymbols?.[name] ?? name; + const specifierTypeOnly = typeOnly || n.isTypeOnly(); + const symbolTarget = mapping.symbolTargetOverrides?.[name] ?? targetPackage; + usedPackages.add(symbolTarget); + addPending(symbolTarget, [resolvedName], specifierTypeOnly); +} +imp.remove(); +changesCount++; +``` + +Also apply the same reorder to the in-place `setModuleSpecifier` branch (lines 106-159): move `renameAllReferences` calls (lines 156-158) before `imp.setModuleSpecifier` (line 136) — though `setModuleSpecifier` doesn't break bindings, it's cleaner to be consistent. + +- [ ] **Step 3: Run tests** + +Run: `pnpm --filter @modelcontextprotocol/codemod test -- test/v1-to-v2/transforms/importPaths.test.ts` + +Expected: all tests pass. + +- [ ] **Step 4: Suggest commit** + +``` +refactor(codemod): reorder importPaths to rename refs before import removal + +findReferencesAsNodes() (used by renameAllReferences) needs the +import binding to still exist. Move rename calls before imp.remove(). +``` + +--- + +### Task 6: Refactor `removedApis.ts` — same reorder pattern + +Same issue: `renameAllReferences` called after import removal. + +**Files:** +- Modify: `packages/codemod/src/migrations/v1-to-v2/transforms/removedApis.ts` +- Verify: `packages/codemod/test/v1-to-v2/transforms/removedApis.test.ts` + +- [ ] **Step 1: Read the relevant sections** + +Read `packages/codemod/src/migrations/v1-to-v2/transforms/removedApis.ts`. + +Find all places where `renameAllReferences` is called and check whether the import binding has already been removed/modified. + +- [ ] **Step 2: Move renames before import removal** + +Apply the same pattern as Task 5: collect or apply renames before the import specifier or declaration is removed. + +- [ ] **Step 3: Run tests** + +Run: `pnpm --filter @modelcontextprotocol/codemod test -- test/v1-to-v2/transforms/removedApis.test.ts` + +Expected: all tests pass. + +- [ ] **Step 4: Suggest commit** + +``` +refactor(codemod): reorder removedApis to rename refs before import removal +``` + +--- + +### Task 7: Simplify `removeUnusedImport` in importUtils.ts + +The `removeUnusedImport` function (lines 116-141) does a manual `forEachDescendant` walk to count references. Replace with `findReferencesAsNodes()`. + +**Files:** +- Modify: `packages/codemod/src/utils/importUtils.ts` +- Verify: `pnpm --filter @modelcontextprotocol/codemod test` + +- [ ] **Step 1: Read the current function** + +Read `packages/codemod/src/utils/importUtils.ts:116-141`. + +- [ ] **Step 2: Rewrite using findReferencesAsNodes** + +```typescript +export function removeUnusedImport(sourceFile: SourceFile, symbolName: string, onlyMcpImports?: boolean): void { + for (const imp of sourceFile.getImportDeclarations()) { + if (onlyMcpImports && !isAnyMcpSpecifier(imp.getModuleSpecifierValue())) continue; + for (const namedImport of imp.getNamedImports()) { + if ((namedImport.getAliasNode()?.getText() ?? namedImport.getName()) === symbolName) { + // Check if the symbol has any non-import references + const refs = namedImport.getNameNode().findReferencesAsNodes() + .filter(n => !Node.isImportSpecifier(n.getParent())); + if (refs.length === 0) { + namedImport.remove(); + if (imp.getNamedImports().length === 0 && !imp.getDefaultImport() && !imp.getNamespaceImport()) { + imp.remove(); + } + } + return; + } + } + } +} +``` + +This eliminates the manual reference-counting `forEachDescendant` walk. + +- [ ] **Step 3: Run all tests** + +Run: `pnpm --filter @modelcontextprotocol/codemod test` + +Expected: all tests pass. `removeUnusedImport` is called by `specSchemaAccess` and `symbolRenames`. + +- [ ] **Step 4: Suggest commit** + +``` +refactor(codemod): use findReferencesAsNodes in removeUnusedImport +``` + +--- + +### Task 8: Full test suite verification and cleanup + +- [ ] **Step 1: Run full test suite** + +Run: `pnpm --filter @modelcontextprotocol/codemod test` + +Expected: all 14 test files pass. + +- [ ] **Step 2: Run typecheck** + +Run: `pnpm --filter @modelcontextprotocol/codemod typecheck` + +Expected: no type errors. + +- [ ] **Step 3: Run lint** + +Run: `pnpm --filter @modelcontextprotocol/codemod lint` + +Expected: no lint errors. + +- [ ] **Step 4: Remove dead code** + +Check if these functions are still used: +- `findNonImportReferences` in specSchemaAccess.ts — should be deleted (Task 4) +- Any unused imports in modified files + +- [ ] **Step 5: Suggest commit** + +``` +chore(codemod): remove dead code after findReferencesAsNodes refactor +``` + +--- + +## Phase 2: Optional tsconfig Loading for Receiver Type Checking + +This phase is independent of Phase 1 and addresses a different class of PR comments: transforms that cannot verify the *receiver* of a method call (e.g., `.tool()` might be on any object, not just `McpServer`). + +### Task 9: Add tsconfig resolution to projectAnalyzer + +**Files:** +- Modify: `packages/codemod/src/utils/projectAnalyzer.ts` +- Modify: `packages/codemod/src/types.ts` +- Modify: `packages/codemod/src/runner.ts` +- Test: `packages/codemod/test/projectAnalyzer.test.ts` + +- [ ] **Step 1: Add `findTsConfig` to projectAnalyzer** + +```typescript +export function findTsConfig(startDir: string): string | undefined { + let dir = path.resolve(startDir); + const root = path.parse(dir).root; + while (true) { + const candidate = path.join(dir, 'tsconfig.json'); + if (existsSync(candidate)) return candidate; + if (dir === root) return undefined; + if (PROJECT_ROOT_MARKERS.some(m => existsSync(path.join(dir, m)))) return undefined; + dir = path.dirname(dir); + } +} +``` + +- [ ] **Step 2: Extend `TransformContext` with optional Project** + +In `packages/codemod/src/types.ts`: + +```typescript +import type { Project, SourceFile } from 'ts-morph'; + +export interface TransformContext { + projectType: 'client' | 'server' | 'both' | 'unknown'; + project?: Project; + hasTypeInfo?: boolean; +} +``` + +- [ ] **Step 3: Modify runner to optionally load tsconfig** + +In `packages/codemod/src/runner.ts`, change Project creation: + +```typescript +import { findTsConfig } from './utils/projectAnalyzer.js'; + +const tsConfigPath = findTsConfig(options.targetDir); +const project = new Project({ + tsConfigFilePath: tsConfigPath, + skipAddingFilesFromTsConfig: true, + compilerOptions: { + allowJs: true, + noEmit: true, + skipLibCheck: true, + ...(tsConfigPath ? {} : { strict: false }), + } +}); + +// ... existing file globbing ... + +const hasTypeInfo = !!tsConfigPath; +const context: TransformContext = { + ...analyzeProject(options.targetDir), + project, + hasTypeInfo, +}; +``` + +Note: `skipAddingFilesFromTsConfig: true` keeps the current behavior of globbing files ourselves. But with a tsconfig, ts-morph resolves module paths and loads declaration files from `node_modules`. + +- [ ] **Step 4: Test with and without tsconfig** + +The existing tests use `new Project({ useInMemoryFileSystem: true })` and pass `TransformContext` without a `project` field. They should continue to work because `project` and `hasTypeInfo` are optional. + +Add a targeted test in `packages/codemod/test/projectAnalyzer.test.ts`: + +```typescript +describe('findTsConfig', () => { + it('should find tsconfig.json in target directory', () => { + const dir = mkdtempSync(join(tmpdir(), 'codemod-')); + writeFileSync(join(dir, 'tsconfig.json'), '{}'); + expect(findTsConfig(dir)).toBe(join(dir, 'tsconfig.json')); + rmSync(dir, { recursive: true }); + }); + + it('should walk up to find tsconfig.json', () => { + const dir = mkdtempSync(join(tmpdir(), 'codemod-')); + const subDir = join(dir, 'src'); + mkdirSync(subDir); + writeFileSync(join(dir, 'tsconfig.json'), '{}'); + expect(findTsConfig(subDir)).toBe(join(dir, 'tsconfig.json')); + rmSync(dir, { recursive: true }); + }); + + it('should return undefined when no tsconfig exists', () => { + const dir = mkdtempSync(join(tmpdir(), 'codemod-')); + mkdirSync(join(dir, '.git')); + expect(findTsConfig(dir)).toBeUndefined(); + rmSync(dir, { recursive: true }); + }); +}); +``` + +- [ ] **Step 5: Suggest commit** + +``` +feat(codemod): optionally resolve tsconfig for type-aware transforms + +When a tsconfig.json is found near the target directory, the ts-morph +Project loads it for module resolution and type information. Transforms +can check context.hasTypeInfo to use type-aware APIs. Falls back to +syntax-only mode when no tsconfig is found. +``` + +--- + +### Task 10: Add receiver type checking to `mcpServerApi.ts` + +When type info is available, verify that `.tool()` / `.prompt()` / `.resource()` calls are on an `McpServer` instance. This addresses the PR comment about false positives on `someOtherObj.tool()`. + +**Files:** +- Modify: `packages/codemod/src/migrations/v1-to-v2/transforms/mcpServerApi.ts` +- Verify: `packages/codemod/test/v1-to-v2/transforms/mcpServerApi.test.ts` + +- [ ] **Step 1: Add a receiver-type guard helper** + +At the top of `mcpServerApi.ts`: + +```typescript +function isMcpServerReceiver(expr: import('ts-morph').PropertyAccessExpression, context: TransformContext): boolean { + if (!context.hasTypeInfo) return true; // permissive when no types + + try { + const receiverType = expr.getExpression().getType(); + const symbol = receiverType.getSymbol(); + if (!symbol) return true; // can't determine — be permissive + const name = symbol.getName(); + return name === 'McpServer'; + } catch { + return true; // type resolution failed — be permissive + } +} +``` + +- [ ] **Step 2: Guard the call collection loop** + +In the switch statement (lines 33-59), add the guard: + +```typescript +for (const call of calls) { + const expr = call.getExpression(); + if (!Node.isPropertyAccessExpression(expr)) continue; + if (!isMcpServerReceiver(expr, context)) continue; // ← NEW + const methodName = expr.getName(); + // ... rest of switch ... +} +``` + +Note: `_context` parameter in `apply()` must be renamed to `context` since it's now used. + +- [ ] **Step 3: Run tests** + +Run: `pnpm --filter @modelcontextprotocol/codemod test -- test/v1-to-v2/transforms/mcpServerApi.test.ts` + +Expected: all tests pass. Tests use in-memory projects without type info, so `isMcpServerReceiver` returns `true` (permissive mode). + +- [ ] **Step 4: Suggest commit** + +``` +feat(codemod): add receiver type checking for McpServer API migration + +When type info is available (tsconfig resolved), verify that .tool(), +.prompt(), .resource() calls are on McpServer instances. Falls back +to permissive mode when types unavailable. +``` + +--- + +## Summary of Changes + +| Metric | Before | After Phase 1 | After Phase 2 | +|--------|--------|---------------|---------------| +| `renameAllReferences` parent guards | 12 | 2 (ShorthandProp, ExportSpecifier) | 2 | +| `contextTypes` parent guards | 4 | 0 | 0 | +| `specSchemaAccess` parent guards | 6 | 3 (pattern classification only) | 3 | +| `forEachDescendant` walks across all transforms | ~12 | ~4 | ~4 | +| Manual import-provenance functions | 6 | 6 (unchanged) | 6 (could reduce further) | +| Receiver type checking | none | none | mcpServerApi | +| Lines in astUtils.ts | 33 | ~28 | ~28 | +| Lines in specSchemaAccess.ts | 350 | ~300 | ~300 | +| Lines in contextTypes.ts | 257 | ~200 | ~200 | +| Lines in symbolRenames.ts | 352 | ~310 | ~310 | + +Phase 1 (Tasks 1-8) is the high-value work. Phase 2 (Tasks 9-10) is additive improvement. diff --git a/docs/superpowers/plans/2026-05-15-codemod-batch-test-fixes.md b/docs/superpowers/plans/2026-05-15-codemod-batch-test-fixes.md new file mode 100644 index 0000000000..48c9d8de26 --- /dev/null +++ b/docs/superpowers/plans/2026-05-15-codemod-batch-test-fixes.md @@ -0,0 +1,549 @@ +# Codemod Batch Test Fixes Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Fix 5 codemod transform issues discovered by running the batch test against real-world repos (inspector + mcp-servers-fork). + +**Architecture:** Each fix targets a specific transform or mapping file within `packages/codemod/src/migrations/v1-to-v2/`. Fixes are ordered by dependency: Tasks 1 and 4 are independent; Tasks 2 and 3 both modify `specSchemaAccess.ts` so Task 2 must land first; Task 5 is independent. All tasks follow TDD. + +**Tech Stack:** TypeScript, ts-morph (AST manipulation), vitest + +--- + +## File Map + +| File | Action | Task(s) | +|------|--------|---------| +| `packages/codemod/src/migrations/v1-to-v2/mappings/schemaToMethodMap.ts` | Modify | 1 | +| `packages/codemod/test/v1-to-v2/transforms/handlerRegistration.test.ts` | Modify | 1 | +| `packages/codemod/src/migrations/v1-to-v2/transforms/specSchemaAccess.ts` | Modify | 2, 3 | +| `packages/codemod/test/v1-to-v2/transforms/specSchemaAccess.test.ts` | Modify | 2, 3 | +| `packages/codemod/src/migrations/v1-to-v2/mappings/importMap.ts` | Modify | 4, 5 | +| `packages/codemod/test/v1-to-v2/transforms/importPaths.test.ts` | Modify | 4, 5 | + +--- + +### Task 1: Complete handler registration schema-to-method mapping + +Add missing experimental/task request schemas and notification schemas to `schemaToMethodMap.ts` so the `handlerRegistration` transform auto-converts them to string method names instead of falling through to `specSchemaAccess` which incorrectly replaces them with `specTypeSchemas.X`. + +**Impact:** Fixes ~20 errors in inspector/client `useConnection.ts` (setRequestHandler + downstream param type inference). + +**Files:** +- Modify: `packages/codemod/src/migrations/v1-to-v2/mappings/schemaToMethodMap.ts` +- Test: `packages/codemod/test/v1-to-v2/transforms/handlerRegistration.test.ts` + +- [ ] **Step 1: Write failing tests for task request schemas** + +Add to `handlerRegistration.test.ts`: + +```typescript +it('replaces ListTasksRequestSchema with method string', () => { + const input = [ + `import { ListTasksRequestSchema } from '@modelcontextprotocol/sdk/types.js';`, + `client.setRequestHandler(ListTasksRequestSchema, async (request) => {`, + ` return { tasks: [] };`, + `});`, + '' + ].join('\n'); + const result = applyTransform(input); + expect(result).toContain("setRequestHandler('tasks/list'"); + expect(result).not.toContain('ListTasksRequestSchema'); +}); + +it('replaces GetTaskRequestSchema with method string', () => { + const input = [ + `import { GetTaskRequestSchema } from '@modelcontextprotocol/sdk/types.js';`, + `client.setRequestHandler(GetTaskRequestSchema, async (request) => {`, + ` return { taskId: '1', status: 'completed' };`, + `});`, + '' + ].join('\n'); + const result = applyTransform(input); + expect(result).toContain("setRequestHandler('tasks/get'"); + expect(result).not.toContain('GetTaskRequestSchema'); +}); + +it('replaces CancelTaskRequestSchema with method string', () => { + const input = [ + `import { CancelTaskRequestSchema } from '@modelcontextprotocol/sdk/types.js';`, + `client.setRequestHandler(CancelTaskRequestSchema, async (request) => {`, + ` return {};`, + `});`, + '' + ].join('\n'); + const result = applyTransform(input); + expect(result).toContain("setRequestHandler('tasks/cancel'"); + expect(result).not.toContain('CancelTaskRequestSchema'); +}); + +it('replaces GetTaskPayloadRequestSchema with method string', () => { + const input = [ + `import { GetTaskPayloadRequestSchema } from '@modelcontextprotocol/sdk/types.js';`, + `client.setRequestHandler(GetTaskPayloadRequestSchema, async (request) => {`, + ` return { content: [] };`, + `});`, + '' + ].join('\n'); + const result = applyTransform(input); + expect(result).toContain("setRequestHandler('tasks/result'"); + expect(result).not.toContain('GetTaskPayloadRequestSchema'); +}); + +it('replaces TaskStatusNotificationSchema with method string', () => { + const input = [ + `import { TaskStatusNotificationSchema } from '@modelcontextprotocol/sdk/types.js';`, + `client.setNotificationHandler(TaskStatusNotificationSchema, async () => {});`, + '' + ].join('\n'); + const result = applyTransform(input); + expect(result).toContain("setNotificationHandler('notifications/tasks/status'"); + expect(result).not.toContain('TaskStatusNotificationSchema'); +}); + +it('replaces ElicitationCompleteNotificationSchema with method string', () => { + const input = [ + `import { ElicitationCompleteNotificationSchema } from '@modelcontextprotocol/sdk/types.js';`, + `client.setNotificationHandler(ElicitationCompleteNotificationSchema, async () => {});`, + '' + ].join('\n'); + const result = applyTransform(input); + expect(result).toContain("setNotificationHandler('notifications/elicitation/complete'"); + expect(result).not.toContain('ElicitationCompleteNotificationSchema'); +}); +``` + +- [ ] **Step 2: Run tests to verify they fail** + +Run: `pnpm --filter @modelcontextprotocol/codemod test -- test/v1-to-v2/transforms/handlerRegistration.test.ts` +Expected: 6 new tests FAIL (schemas not in map, get "Custom method handler" diagnostic instead) + +- [ ] **Step 3: Add missing schemas to the mapping** + +In `packages/codemod/src/migrations/v1-to-v2/mappings/schemaToMethodMap.ts`, add entries to `SCHEMA_TO_METHOD`: + +```typescript +ListTasksRequestSchema: 'tasks/list', +GetTaskRequestSchema: 'tasks/get', +GetTaskPayloadRequestSchema: 'tasks/result', +CancelTaskRequestSchema: 'tasks/cancel', +``` + +And add entries to `NOTIFICATION_SCHEMA_TO_METHOD`: + +```typescript +TaskStatusNotificationSchema: 'notifications/tasks/status', +ElicitationCompleteNotificationSchema: 'notifications/elicitation/complete', +``` + +- [ ] **Step 4: Run tests to verify they pass** + +Run: `pnpm --filter @modelcontextprotocol/codemod test -- test/v1-to-v2/transforms/handlerRegistration.test.ts` +Expected: All tests PASS + +- [ ] **Step 5: Commit** + +```bash +git add packages/codemod/src/migrations/v1-to-v2/mappings/schemaToMethodMap.ts packages/codemod/test/v1-to-v2/transforms/handlerRegistration.test.ts +git commit -m "fix(codemod): add task and elicitation schemas to handler registration map" +``` + +--- + +### Task 2: Replace schema identifiers in generic property access positions + +Currently, when a spec schema like `OAuthTokensSchema` is used with a Zod-specific method (e.g., `.parseAsync()`, `.or()`, `.extend()`), the `specSchemaAccess` transform only emits a diagnostic but does NOT replace the identifier. This leaves the old schema name in imports, which breaks compilation since v2 packages don't export these schema symbols. + +**Fix:** In the generic property access case, replace the identifier with `specTypeSchemas.X` (even though the method call itself won't work). The diagnostic still tells the user what to do, but the import now resolves. + +**Impact:** Fixes ~12 "Module has no exported member 'XSchema'" errors across both repos. + +**Files:** +- Modify: `packages/codemod/src/migrations/v1-to-v2/transforms/specSchemaAccess.ts` +- Test: `packages/codemod/test/v1-to-v2/transforms/specSchemaAccess.test.ts` + +- [ ] **Step 1: Write failing tests for generic property access replacement** + +Add to `specSchemaAccess.test.ts` in a new `describe` block: + +```typescript +describe('auto-transform: generic property access → specTypeSchemas.X', () => { + it('replaces schema identifier in .parseAsync() call', () => { + const input = [ + `import { OAuthTokensSchema } from '@modelcontextprotocol/server';`, + `const tokens = await OAuthTokensSchema.parseAsync(data);`, + '' + ].join('\n'); + const { text, result } = applyTransform(input); + expect(text).toContain('specTypeSchemas.OAuthTokens.parseAsync(data)'); + expect(text).not.toMatch(/import\s*\{[^}]*OAuthTokensSchema[^}]*\}/); + expect(result.changesCount).toBeGreaterThan(0); + expect(result.diagnostics.length).toBeGreaterThan(0); + }); + + it('replaces schema identifier in .or() call', () => { + const input = [ + `import { ServerNotificationSchema } from '@modelcontextprotocol/server';`, + `const union = ServerNotificationSchema.or(otherSchema);`, + '' + ].join('\n'); + const { text, result } = applyTransform(input); + expect(text).toContain('specTypeSchemas.ServerNotification.or(otherSchema)'); + expect(text).not.toMatch(/import\s*\{[^}]*ServerNotificationSchema[^}]*\}/); + expect(result.changesCount).toBeGreaterThan(0); + }); + + it('replaces schema identifier in .extend() call', () => { + const input = [ + `import { ToolSchema } from '@modelcontextprotocol/server';`, + `const extended = ToolSchema.extend({ extra: z.string() });`, + '' + ].join('\n'); + const { text, result } = applyTransform(input); + expect(text).toContain('specTypeSchemas.Tool.extend'); + expect(result.changesCount).toBeGreaterThan(0); + }); + + it('adds specTypeSchemas import for generic property access', () => { + const input = [ + `import { OAuthTokensSchema } from '@modelcontextprotocol/server';`, + `const tokens = await OAuthTokensSchema.parseAsync(data);`, + '' + ].join('\n'); + const { text } = applyTransform(input); + expect(text).toMatch(/import.*specTypeSchemas.*from/); + }); +}); +``` + +- [ ] **Step 2: Run tests to verify they fail** + +Run: `pnpm --filter @modelcontextprotocol/codemod test -- test/v1-to-v2/transforms/specSchemaAccess.test.ts` +Expected: 4 new tests FAIL (generic property access only emits diagnostic, doesn't replace) + +- [ ] **Step 3: Modify the generic property access handler to also replace the identifier** + +In `packages/codemod/src/migrations/v1-to-v2/transforms/specSchemaAccess.ts`, find the generic property access handler in `handleReference()` (around line 129). Change: + +```typescript +// BEFORE (diagnostic-only, no replacement): +if (parent && Node.isPropertyAccessExpression(parent) && parent.getExpression() === ref) { + diagnostics.push( + warning( + sourceFile.getFilePath(), + ref.getStartLineNumber(), + `${localName} is not exported in v2. Use \`specTypeSchemas.${typeName}\` (typed as StandardSchemaV1) or \`isSpecType.${typeName}\` for validation.` + ) + ); + return false; +} +``` + +to: + +```typescript +// AFTER (replace identifier AND emit diagnostic): +if (parent && Node.isPropertyAccessExpression(parent) && parent.getExpression() === ref) { + const line = ref.getStartLineNumber(); + ref.replaceWithText(`specTypeSchemas.${typeName}`); + ensureImport(sourceFile, 'specTypeSchemas'); + diagnostics.push( + warning( + sourceFile.getFilePath(), + line, + `Replaced ${localName} with specTypeSchemas.${typeName}. Note: typed as StandardSchemaV1, not ZodType — Zod methods like .safeParse()/.parse()/.parseAsync() are not available. Manual rewrite required.` + ) + ); + return true; +} +``` + +- [ ] **Step 4: Run tests to verify they pass** + +Run: `pnpm --filter @modelcontextprotocol/codemod test -- test/v1-to-v2/transforms/specSchemaAccess.test.ts` +Expected: All tests PASS (including existing "keeps original schema import when some refs are diagnostic-only" test — verify this one still passes since the behavior changed) + +**Note:** The existing test at line 262 ("keeps original schema import when some refs are diagnostic-only") combines a `.safeParse().success` auto-transform with a `.parse()` diagnostic-only case. The `.parse()` case is separate from the generic property access case (it has its own handler returning `false`). This test should still pass because `.parse()` is handled before the generic property access check. + +- [ ] **Step 5: Commit** + +```bash +git add packages/codemod/src/migrations/v1-to-v2/transforms/specSchemaAccess.ts packages/codemod/test/v1-to-v2/transforms/specSchemaAccess.test.ts +git commit -m "fix(codemod): replace schema identifiers in generic property access positions" +``` + +--- + +### Task 3: Fix safeParse-to-validate `.error` sub-property remapping + +When `const r = XSchema.safeParse(v)` is captured, the transform rewrites `.error` → `.issues`. But downstream accesses like `r.error.message` become `r.issues.message` (wrong — `.issues` is an array) and `r.error.issues` becomes `r.issues.issues` (double nesting). + +**Fix:** In the `case 'error':` block of `rewriteCapturedSafeParse`, check if the parent node is another PropertyAccessExpression (meaning `r.error.X`). Handle `.issues` (unwrap) and `.message` (rewrite to array map) specifically. + +**Impact:** Fixes ~10 TypeScript errors in inspector/client's `AppRenderer.tsx`, `SamplingRequest.tsx`, `ToolResults.tsx`. + +**Files:** +- Modify: `packages/codemod/src/migrations/v1-to-v2/transforms/specSchemaAccess.ts` +- Test: `packages/codemod/test/v1-to-v2/transforms/specSchemaAccess.test.ts` + +- [ ] **Step 1: Write failing tests for error sub-property remapping** + +Add to `specSchemaAccess.test.ts` inside the "auto-transform: captured safeParse result" describe block: + +```typescript +it('rewrites .error.issues to .issues (unwrap double nesting)', () => { + const input = [ + `import { CallToolResultSchema } from '@modelcontextprotocol/server';`, + `const parsed = CallToolResultSchema.safeParse(data);`, + `if (!parsed.success) { console.log(parsed.error.issues); }`, + '' + ].join('\n'); + const { text } = applyTransform(input); + expect(text).toContain('parsed.issues'); + expect(text).not.toContain('parsed.issues.issues'); + expect(text).not.toContain('parsed.error'); +}); + +it('rewrites .error.message to issues map expression', () => { + const input = [ + `import { CallToolResultSchema } from '@modelcontextprotocol/server';`, + `const parsed = CallToolResultSchema.safeParse(data);`, + `if (!parsed.success) { console.log(parsed.error.message); }`, + '' + ].join('\n'); + const { text } = applyTransform(input); + expect(text).not.toContain('parsed.error'); + expect(text).not.toContain('parsed.issues.message'); + expect(text).toContain("parsed.issues?.map(i => i.message).join(', ')"); +}); + +it('rewrites bare .error to .issues (unchanged behavior)', () => { + const input = [ + `import { ToolSchema } from '@modelcontextprotocol/server';`, + `const result = ToolSchema.safeParse(raw);`, + `if (!result.success) { console.log(result.error); }`, + '' + ].join('\n'); + const { text } = applyTransform(input); + expect(text).toContain('result.issues'); + expect(text).not.toContain('result.error'); +}); +``` + +- [ ] **Step 2: Run tests to verify they fail** + +Run: `pnpm --filter @modelcontextprotocol/codemod test -- test/v1-to-v2/transforms/specSchemaAccess.test.ts` +Expected: First 2 new tests FAIL (`.error.issues` becomes `.issues.issues`, `.error.message` becomes `.issues.message`). Third test should already pass. + +- [ ] **Step 3: Update the error case in rewriteCapturedSafeParse** + +In `packages/codemod/src/migrations/v1-to-v2/transforms/specSchemaAccess.ts`, in the `rewriteCapturedSafeParse` function, replace the `case 'error'` block (around line 293): + +```typescript +// BEFORE: +case 'error': { + replacements.push({ node, newText: `${varName}.issues` }); + break; +} +``` + +with: + +```typescript +// AFTER: +case 'error': { + const errorParent = node.getParent(); + if (errorParent && Node.isPropertyAccessExpression(errorParent) && errorParent.getExpression() === node) { + const subProp = errorParent.getName(); + if (subProp === 'issues') { + replacements.push({ node: errorParent, newText: `${varName}.issues` }); + } else if (subProp === 'message') { + replacements.push({ node: errorParent, newText: `${varName}.issues?.map(i => i.message).join(', ')` }); + } else { + replacements.push({ node: errorParent, newText: `${varName}.issues` }); + } + } else { + replacements.push({ node, newText: `${varName}.issues` }); + } + break; +} +``` + +- [ ] **Step 4: Run tests to verify they pass** + +Run: `pnpm --filter @modelcontextprotocol/codemod test -- test/v1-to-v2/transforms/specSchemaAccess.test.ts` +Expected: All tests PASS + +- [ ] **Step 5: Commit** + +```bash +git add packages/codemod/src/migrations/v1-to-v2/transforms/specSchemaAccess.ts packages/codemod/test/v1-to-v2/transforms/specSchemaAccess.test.ts +git commit -m "fix(codemod): handle .error sub-property accesses in safeParse rewrite" +``` + +--- + +### Task 4: Handle `zod-compat.js` import path + +The import path `@modelcontextprotocol/sdk/server/zod-compat.js` is not in `IMPORT_MAP`, so `importPaths` emits "Unknown SDK import path" and leaves it untouched. The file exported `AnySchema` and `SchemaOutput` types that don't exist in v2. + +**Fix:** Add the path to `IMPORT_MAP` as `removed` with a descriptive message. This removes the import and emits a clear diagnostic. + +**Impact:** Fixes "Unknown SDK import path" warnings in inspector/client (4 files). The `AnySchema`/`SchemaOutput` usages in function signatures will still need manual migration, but the import won't be stale. + +**Files:** +- Modify: `packages/codemod/src/migrations/v1-to-v2/mappings/importMap.ts` +- Test: `packages/codemod/test/v1-to-v2/transforms/importPaths.test.ts` + +- [ ] **Step 1: Write failing test for zod-compat import removal** + +Add to `importPaths.test.ts`: + +```typescript +it('removes zod-compat.js import and emits diagnostic', () => { + const input = [ + `import { AnySchema, SchemaOutput } from '@modelcontextprotocol/sdk/server/zod-compat.js';`, + `function validate(schema: T): SchemaOutput { return {} as any; }`, + '' + ].join('\n'); + const project = new Project({ useInMemoryFileSystem: true }); + const sourceFile = project.createSourceFile('test.ts', input); + const result = importPathsTransform.apply(sourceFile, ctx); + const text = sourceFile.getFullText(); + expect(text).not.toContain('zod-compat'); + expect(text).not.toContain("from '@modelcontextprotocol/sdk"); + expect(result.changesCount).toBeGreaterThan(0); + expect(result.diagnostics.length).toBeGreaterThan(0); + expect(result.diagnostics[0]!.message).toContain('zod-compat'); +}); +``` + +Ensure the test file imports the necessary pieces — check the existing test imports at the top and match them. The existing test file should already import `importPathsTransform`, `Project`, and define a `ctx` constant. + +- [ ] **Step 2: Run test to verify it fails** + +Run: `pnpm --filter @modelcontextprotocol/codemod test -- test/v1-to-v2/transforms/importPaths.test.ts` +Expected: FAIL — import is left unchanged, "Unknown SDK import path" warning emitted + +- [ ] **Step 3: Add zod-compat.js to the import map** + +In `packages/codemod/src/migrations/v1-to-v2/mappings/importMap.ts`, add this entry to `IMPORT_MAP` after the `'@modelcontextprotocol/sdk/server/middleware.js'` entry: + +```typescript +'@modelcontextprotocol/sdk/server/zod-compat.js': { + target: '', + status: 'removed', + removalMessage: + 'zod-compat removed in v2. AnySchema and SchemaOutput types have no v2 equivalent — v2 uses StandardSchemaV1 from @standard-schema/spec. Rewrite generic function signatures to use StandardSchemaV1 directly.' +}, +``` + +- [ ] **Step 4: Run test to verify it passes** + +Run: `pnpm --filter @modelcontextprotocol/codemod test -- test/v1-to-v2/transforms/importPaths.test.ts` +Expected: All tests PASS + +- [ ] **Step 5: Commit** + +```bash +git add packages/codemod/src/migrations/v1-to-v2/mappings/importMap.ts packages/codemod/test/v1-to-v2/transforms/importPaths.test.ts +git commit -m "fix(codemod): handle zod-compat.js import path as removed" +``` + +--- + +### Task 5: Rename `ResourceTemplate` type imports to `ResourceTemplateType` + +When `ResourceTemplate` is imported from `@modelcontextprotocol/sdk/types.js` (protocol type usage), the import is rewritten to `@modelcontextprotocol/server`. But the server exports a `ResourceTemplate` **class** (used for server-side registration), shadowing the protocol type. The protocol type already exists in v2 as `ResourceTemplateType` (defined in `core/src/types/types.ts`, publicly exported via `core/public`'s `export * from '../../types/types.js'`, and re-exported by both `@modelcontextprotocol/server` and `@modelcontextprotocol/client`). + +**Fix:** Add `ResourceTemplate` → `ResourceTemplateType` to the `renamedSymbols` mapping for the `types.js` import path. This auto-renames the import and all references. No SDK changes needed — `ResourceTemplateType` is already publicly exported. + +**Impact:** Fixes ~8 TypeScript errors in inspector/client `ResourcesTab.tsx` (`.name`, `.description`, `UriTemplate` vs `string` issues). + +**Files:** +- Modify: `packages/codemod/src/migrations/v1-to-v2/mappings/importMap.ts` +- Test: `packages/codemod/test/v1-to-v2/transforms/importPaths.test.ts` + +- [ ] **Step 1: Write failing test for ResourceTemplate rename** + +Add to `importPaths.test.ts`: + +```typescript +it('renames ResourceTemplate to ResourceTemplateType when imported from types.js', () => { + const input = [ + `import { ResourceTemplate } from '@modelcontextprotocol/sdk/types.js';`, + `const template: ResourceTemplate = getTemplate();`, + '' + ].join('\n'); + const project = new Project({ useInMemoryFileSystem: true }); + const sourceFile = project.createSourceFile('test.ts', input); + const result = importPathsTransform.apply(sourceFile, ctx); + const text = sourceFile.getFullText(); + expect(text).toContain('ResourceTemplateType'); + expect(text).not.toMatch(/\bResourceTemplate\b(?!Type)/); + expect(result.changesCount).toBeGreaterThan(0); +}); +``` + +- [ ] **Step 2: Run test to verify it fails** + +Run: `pnpm --filter @modelcontextprotocol/codemod test -- test/v1-to-v2/transforms/importPaths.test.ts` +Expected: FAIL — ResourceTemplate is not renamed + +- [ ] **Step 3: Add ResourceTemplate rename to import map** + +In `packages/codemod/src/migrations/v1-to-v2/mappings/importMap.ts`, find the entry for `'@modelcontextprotocol/sdk/types.js'`: + +```typescript +'@modelcontextprotocol/sdk/types.js': { + target: 'RESOLVE_BY_CONTEXT', + status: 'moved' +}, +``` + +Add `renamedSymbols`: + +```typescript +'@modelcontextprotocol/sdk/types.js': { + target: 'RESOLVE_BY_CONTEXT', + status: 'moved', + renamedSymbols: { + ResourceTemplate: 'ResourceTemplateType' + } +}, +``` + +- [ ] **Step 4: Run test to verify it passes** + +Run: `pnpm --filter @modelcontextprotocol/codemod test -- test/v1-to-v2/transforms/importPaths.test.ts` +Expected: All tests PASS + +- [ ] **Step 5: Run full test suite** + +Run: `pnpm --filter @modelcontextprotocol/codemod test` +Expected: All tests PASS across all test files + +- [ ] **Step 6: Commit** + +```bash +git add packages/codemod/src/migrations/v1-to-v2/mappings/importMap.ts packages/codemod/test/v1-to-v2/transforms/importPaths.test.ts +git commit -m "fix(codemod): rename ResourceTemplate to ResourceTemplateType to avoid class collision" +``` + +--- + +## Verification + +After all 5 tasks are complete: + +- [ ] **Rebuild and re-run batch test** + +```bash +pnpm --filter @modelcontextprotocol/codemod build +pnpm --filter @modelcontextprotocol/codemod batch-test +``` + +Compare `packages/codemod/batch-test/results/summary.json` with the pre-fix results. Expected improvements: +- inspector/client: build errors should decrease significantly (StandardSchemaV1→AnySchema errors from handler registration fixed, schema import errors fixed) +- inspector/server: `SSEServerTransport` errors remain (manual migration), but `setRequestHandler` task schema errors should be fixed +- mcp-servers-fork: `SSEServerTransport` errors remain (manual migration), test context mock errors remain (manual migration) diff --git a/docs/superpowers/plans/2026-06-02-readbuffer-max-size.md b/docs/superpowers/plans/2026-06-02-readbuffer-max-size.md new file mode 100644 index 0000000000..867e8a3599 --- /dev/null +++ b/docs/superpowers/plans/2026-06-02-readbuffer-max-size.md @@ -0,0 +1,323 @@ +# ReadBuffer Max Size Guard Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Add a configurable maximum buffer size to `ReadBuffer` to prevent unbounded memory growth from a misbehaving stdio peer (GHSA-wqgc-pwpr-pq7r). + +**Architecture:** `ReadBuffer.append()` gains a size guard that throws on overflow. Both stdio transports wrap their data handlers in try/catch to catch the throw, report via `onerror`, and close the transport. The constant and constructor option are exported as public API. + +**Tech Stack:** TypeScript, vitest + +--- + +### Task 1: Add size guard to ReadBuffer + +**Files:** +- Modify: `packages/core/src/shared/stdio.ts:1-42` + +- [ ] **Step 1: Write failing tests for buffer overflow** + +Add a new `describe` block to `packages/core/test/shared/stdio.test.ts`: + +```typescript +describe('buffer size limit', () => { + test('should throw when buffer exceeds default max size', () => { + const readBuffer = new ReadBuffer(); + const chunk = Buffer.alloc(1024 * 1024); // 1 MB + // Default is 10 MB, so 11 appends should fail + for (let i = 0; i < 10; i++) { + readBuffer.append(chunk); + } + expect(() => readBuffer.append(chunk)).toThrow( + /ReadBuffer exceeded maximum size/ + ); + }); + + test('should throw when buffer exceeds custom max size', () => { + const readBuffer = new ReadBuffer({ maxBufferSize: 100 }); + readBuffer.append(Buffer.alloc(50)); + expect(() => readBuffer.append(Buffer.alloc(51))).toThrow( + /ReadBuffer exceeded maximum size/ + ); + }); + + test('should clear buffer before throwing on overflow', () => { + const readBuffer = new ReadBuffer({ maxBufferSize: 100 }); + readBuffer.append(Buffer.alloc(50)); + expect(() => readBuffer.append(Buffer.alloc(51))).toThrow(); + + // Buffer should be cleared — can append again + readBuffer.append(Buffer.alloc(50)); + // And read messages normally + expect(readBuffer.readMessage()).toBeNull(); + }); + + test('should allow appending up to exactly the max size', () => { + const readBuffer = new ReadBuffer({ maxBufferSize: 100 }); + // Should not throw — exactly at limit + expect(() => readBuffer.append(Buffer.alloc(100))).not.toThrow(); + }); + + test('should work with no options (backwards compatible)', () => { + const readBuffer = new ReadBuffer(); + // Small append should always work + readBuffer.append(Buffer.from('hello\n')); + expect(readBuffer.readMessage()).not.toBeNull(); + }); +}); +``` + +- [ ] **Step 2: Run the tests to confirm they fail** + +Run: `pnpm --filter @modelcontextprotocol/core test -- packages/core/test/shared/stdio.test.ts` +Expected: FAIL — `ReadBuffer` constructor doesn't accept options yet. + +- [ ] **Step 3: Implement the size guard in ReadBuffer** + +Modify `packages/core/src/shared/stdio.ts`. The full file should become: + +```typescript +import type { JSONRPCMessage } from '../types/index.js'; +import { JSONRPCMessageSchema } from '../types/index.js'; + +export const DEFAULT_MAX_BUFFER_SIZE = 10 * 1024 * 1024; // 10 MB + +/** + * Buffers a continuous stdio stream into discrete JSON-RPC messages. + */ +export class ReadBuffer { + private _buffer?: Buffer; + private _maxBufferSize: number; + + constructor(options?: { maxBufferSize?: number }) { + this._maxBufferSize = options?.maxBufferSize ?? DEFAULT_MAX_BUFFER_SIZE; + } + + append(chunk: Buffer): void { + const newSize = (this._buffer?.length ?? 0) + chunk.length; + if (newSize > this._maxBufferSize) { + this.clear(); + throw new Error( + `ReadBuffer exceeded maximum size of ${this._maxBufferSize} bytes` + ); + } + this._buffer = this._buffer ? Buffer.concat([this._buffer, chunk]) : chunk; + } + + readMessage(): JSONRPCMessage | null { + while (this._buffer) { + const index = this._buffer.indexOf('\n'); + if (index === -1) { + return null; + } + + const line = this._buffer.toString('utf8', 0, index).replace(/\r$/, ''); + this._buffer = this._buffer.subarray(index + 1); + + try { + return deserializeMessage(line); + } catch (error) { + // Skip non-JSON lines (e.g., debug output from hot-reload tools like + // tsx or nodemon that write to stdout). Schema validation errors still + // throw so malformed-but-valid-JSON messages surface via onerror. + if (error instanceof SyntaxError) { + continue; + } + throw error; + } + } + return null; + } + + clear(): void { + this._buffer = undefined; + } +} + +export function deserializeMessage(line: string): JSONRPCMessage { + return JSONRPCMessageSchema.parse(JSON.parse(line)); +} + +export function serializeMessage(message: JSONRPCMessage): string { + return JSON.stringify(message) + '\n'; +} +``` + +- [ ] **Step 4: Run the tests to confirm they pass** + +Run: `pnpm --filter @modelcontextprotocol/core test -- packages/core/test/shared/stdio.test.ts` +Expected: All tests PASS (including all existing tests — backwards compatible). + +- [ ] **Step 5: Commit** + +```bash +git add packages/core/src/shared/stdio.ts packages/core/test/shared/stdio.test.ts +git commit -m "fix(core): add max buffer size guard to ReadBuffer + +Prevents unbounded memory growth when a stdio peer sends data without +newline delimiters. Default limit is 10 MB, configurable via constructor. + +Ref: GHSA-wqgc-pwpr-pq7r" +``` + +--- + +### Task 2: Add DEFAULT_MAX_BUFFER_SIZE to public exports + +**Files:** +- Modify: `packages/core/src/exports/public/index.ts:70` + +- [ ] **Step 1: Add the constant to the public export** + +Change line 70 in `packages/core/src/exports/public/index.ts` from: + +```typescript +export { deserializeMessage, ReadBuffer, serializeMessage } from '../../shared/stdio.js'; +``` + +to: + +```typescript +export { DEFAULT_MAX_BUFFER_SIZE, deserializeMessage, ReadBuffer, serializeMessage } from '../../shared/stdio.js'; +``` + +- [ ] **Step 2: Run typecheck to confirm it compiles** + +Run: `pnpm --filter @modelcontextprotocol/core typecheck` +Expected: No errors. + +- [ ] **Step 3: Commit** + +```bash +git add packages/core/src/exports/public/index.ts +git commit -m "feat(core): export DEFAULT_MAX_BUFFER_SIZE from public API" +``` + +--- + +### Task 3: Add try/catch to StdioClientTransport data handler + +**Files:** +- Modify: `packages/client/src/client/stdio.ts:151-154` + +- [ ] **Step 1: Wrap the data handler in try/catch** + +Change lines 151-154 of `packages/client/src/client/stdio.ts` from: + +```typescript + this._process.stdout?.on('data', chunk => { + this._readBuffer.append(chunk); + this.processReadBuffer(); + }); +``` + +to: + +```typescript + this._process.stdout?.on('data', chunk => { + try { + this._readBuffer.append(chunk); + this.processReadBuffer(); + } catch (error) { + this.onerror?.(error as Error); + this.close().catch(() => {}); + } + }); +``` + +- [ ] **Step 2: Run typecheck** + +Run: `pnpm --filter @modelcontextprotocol/client typecheck` +Expected: No errors. + +- [ ] **Step 3: Run existing stdio client tests to verify no regression** + +Run: `pnpm --filter @modelcontextprotocol/client test -- packages/client/test/client/stdio.test.ts` +Expected: All existing tests PASS. + +- [ ] **Step 4: Commit** + +```bash +git add packages/client/src/client/stdio.ts +git commit -m "fix(client): catch ReadBuffer overflow in StdioClientTransport data handler + +Prevents an uncaught exception when ReadBuffer.append() throws due to +exceeding the max buffer size. Routes the error to onerror and closes +the transport. + +Ref: GHSA-wqgc-pwpr-pq7r" +``` + +--- + +### Task 4: Add try/catch to StdioServerTransport data handler + +**Files:** +- Modify: `packages/server/src/server/stdio.ts:34-37` + +- [ ] **Step 1: Wrap the _ondata handler in try/catch** + +Change lines 34-37 of `packages/server/src/server/stdio.ts` from: + +```typescript + _ondata = (chunk: Buffer) => { + this._readBuffer.append(chunk); + this.processReadBuffer(); + }; +``` + +to: + +```typescript + _ondata = (chunk: Buffer) => { + try { + this._readBuffer.append(chunk); + this.processReadBuffer(); + } catch (error) { + this.onerror?.(error as Error); + this.close().catch(() => {}); + } + }; +``` + +- [ ] **Step 2: Run typecheck** + +Run: `pnpm --filter @modelcontextprotocol/server typecheck` +Expected: No errors. + +- [ ] **Step 3: Run existing stdio server tests to verify no regression** + +Run: `pnpm --filter @modelcontextprotocol/server test -- packages/server/test/server/stdio.test.ts` +Expected: All existing tests PASS. + +- [ ] **Step 4: Commit** + +```bash +git add packages/server/src/server/stdio.ts +git commit -m "fix(server): catch ReadBuffer overflow in StdioServerTransport data handler + +Prevents an uncaught exception when ReadBuffer.append() throws due to +exceeding the max buffer size. Routes the error to onerror and closes +the transport. + +Ref: GHSA-wqgc-pwpr-pq7r" +``` + +--- + +### Task 5: Full test suite verification + +- [ ] **Step 1: Run full typecheck across all packages** + +Run: `pnpm typecheck:all` +Expected: No errors. + +- [ ] **Step 2: Run full test suite** + +Run: `pnpm test:all` +Expected: All tests PASS. + +- [ ] **Step 3: Run lint** + +Run: `pnpm lint:all` +Expected: No errors (or fix any formatting issues with `pnpm lint:fix:all`). diff --git a/docs/superpowers/plans/2026-06-02-v1-readbuffer-max-size.md b/docs/superpowers/plans/2026-06-02-v1-readbuffer-max-size.md new file mode 100644 index 0000000000..cfbbfb23b9 --- /dev/null +++ b/docs/superpowers/plans/2026-06-02-v1-readbuffer-max-size.md @@ -0,0 +1,356 @@ +# V1 ReadBuffer Max Size Guard Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Port the ReadBuffer max size guard from the v2 branch (`fix/stdio-buffer-limit`, commit `08780873`) to v1. This prevents unbounded memory growth when a misbehaving stdio peer sends data without newline delimiters (GHSA-wqgc-pwpr-pq7r). + +**Architecture:** `ReadBuffer.append()` gains a size guard that throws on overflow. Both stdio transports wrap their data handlers in try/catch to catch the throw, report via `onerror`, and close the transport. The constant `STDIO_DEFAULT_MAX_BUFFER_SIZE` is exported from `src/shared/stdio.ts`. + +**Tech Stack:** TypeScript, vitest + +**Key difference from v2:** V1 is a flat `src/` layout (not a monorepo under `packages/`). There is no public re-export index file, so the constant is only exported from `src/shared/stdio.ts` directly. + +--- + +### Task 1: Add size guard to ReadBuffer + +**Files:** +- Modify: `src/shared/stdio.ts` +- Modify: `test/shared/stdio.test.ts` + +- [ ] **Step 1: Add buffer size limit tests** + +Append the following to `test/shared/stdio.test.ts`: + +```typescript +import { STDIO_DEFAULT_MAX_BUFFER_SIZE } from '../../src/shared/stdio.js'; + +describe('buffer size limit', () => { + test('should throw when buffer exceeds default max size', () => { + const readBuffer = new ReadBuffer(); + const chunkSize = 1024 * 1024; // 1 MB + const chunk = Buffer.alloc(chunkSize); + const chunksToFill = Math.floor(STDIO_DEFAULT_MAX_BUFFER_SIZE / chunkSize); + for (let i = 0; i < chunksToFill; i++) { + readBuffer.append(chunk); + } + expect(() => readBuffer.append(chunk)).toThrow( + /ReadBuffer exceeded maximum size/ + ); + }); + + test('should throw when buffer exceeds custom max size', () => { + const readBuffer = new ReadBuffer({ maxBufferSize: 100 }); + readBuffer.append(Buffer.alloc(50)); + expect(() => readBuffer.append(Buffer.alloc(51))).toThrow( + /ReadBuffer exceeded maximum size/ + ); + }); + + test('should clear buffer before throwing on overflow', () => { + const readBuffer = new ReadBuffer({ maxBufferSize: 100 }); + readBuffer.append(Buffer.alloc(50)); + expect(() => readBuffer.append(Buffer.alloc(51))).toThrow(); + + // Buffer should be cleared — can append again + readBuffer.append(Buffer.alloc(50)); + // And read messages normally + expect(readBuffer.readMessage()).toBeNull(); + }); + + test('should allow appending up to exactly the max size', () => { + const readBuffer = new ReadBuffer({ maxBufferSize: 100 }); + // Should not throw — exactly at limit + expect(() => readBuffer.append(Buffer.alloc(100))).not.toThrow(); + }); + + test('should work with no options (backwards compatible)', () => { + const readBuffer = new ReadBuffer(); + // Small append should always work + readBuffer.append(Buffer.from(JSON.stringify({ jsonrpc: '2.0', method: 'ping' }) + '\n')); + expect(readBuffer.readMessage()).not.toBeNull(); + }); +}); +``` + +Also update the existing import at the top of the file — change: + +```typescript +import { ReadBuffer } from '../../src/shared/stdio.js'; +``` + +to: + +```typescript +import { STDIO_DEFAULT_MAX_BUFFER_SIZE, ReadBuffer } from '../../src/shared/stdio.js'; +``` + +- [ ] **Step 2: Run tests to confirm they fail** + +Run: `npx vitest test/shared/stdio.test.ts --run` +Expected: FAIL — `ReadBuffer` constructor doesn't accept options yet, `STDIO_DEFAULT_MAX_BUFFER_SIZE` doesn't exist. + +- [ ] **Step 3: Implement the size guard in ReadBuffer** + +Modify `src/shared/stdio.ts`. Add the constant and constructor, and add the size guard to `append()`. The full file should become: + +```typescript +import { JSONRPCMessage, JSONRPCMessageSchema } from '../types.js'; + +export const STDIO_DEFAULT_MAX_BUFFER_SIZE = 10 * 1024 * 1024; + +/** + * Buffers a continuous stdio stream into discrete JSON-RPC messages. + */ +export class ReadBuffer { + private _buffer?: Buffer; + private _maxBufferSize: number; + + constructor(options?: { maxBufferSize?: number }) { + this._maxBufferSize = options?.maxBufferSize ?? STDIO_DEFAULT_MAX_BUFFER_SIZE; + } + + append(chunk: Buffer): void { + const newSize = (this._buffer?.length ?? 0) + chunk.length; + if (newSize > this._maxBufferSize) { + this.clear(); + throw new Error( + `ReadBuffer exceeded maximum size of ${this._maxBufferSize} bytes` + ); + } + this._buffer = this._buffer ? Buffer.concat([this._buffer, chunk]) : chunk; + } + + readMessage(): JSONRPCMessage | null { + if (!this._buffer) { + return null; + } + + const index = this._buffer.indexOf('\n'); + if (index === -1) { + return null; + } + + const line = this._buffer.toString('utf8', 0, index).replace(/\r$/, ''); + this._buffer = this._buffer.subarray(index + 1); + return deserializeMessage(line); + } + + clear(): void { + this._buffer = undefined; + } +} + +export function deserializeMessage(line: string): JSONRPCMessage { + return JSONRPCMessageSchema.parse(JSON.parse(line)); +} + +export function serializeMessage(message: JSONRPCMessage): string { + return JSON.stringify(message) + '\n'; +} +``` + +- [ ] **Step 4: Run tests to confirm they pass** + +Run: `npx vitest test/shared/stdio.test.ts --run` +Expected: All tests PASS. + +- [ ] **Step 5: Suggest commit** + +```bash +git add src/shared/stdio.ts test/shared/stdio.test.ts +git commit -m "fix: add max buffer size guard to ReadBuffer + +Prevents unbounded memory growth when a stdio peer sends data without +newline delimiters. Default limit is 10 MB, configurable via constructor. + +Ref: GHSA-wqgc-pwpr-pq7r" +``` + +--- + +### Task 2: Add try/catch to StdioServerTransport data handler + +**Files:** +- Modify: `src/server/stdio.ts:26-29` +- Modify: `test/server/stdio.test.ts` + +- [ ] **Step 1: Add overflow test for StdioServerTransport** + +Append the following test to `test/server/stdio.test.ts`: + +```typescript +test('should fire onerror and close when ReadBuffer overflows', async () => { + const server = new StdioServerTransport(input, output); + + let receivedError: Error | undefined; + server.onerror = err => { + receivedError = err; + }; + let closeCount = 0; + server.onclose = () => { + closeCount++; + }; + + await server.start(); + + // Push data exceeding the default 10 MB limit without a newline + const chunk = Buffer.alloc(11 * 1024 * 1024, 0x41); + input.push(chunk); + + // Allow the close() promise to settle + await new Promise(resolve => setTimeout(resolve, 10)); + + expect(receivedError?.message).toMatch(/ReadBuffer exceeded maximum size/); + expect(closeCount).toBe(1); +}); +``` + +- [ ] **Step 2: Run to confirm the test fails** + +Run: `npx vitest test/server/stdio.test.ts --run` +Expected: FAIL — the uncaught throw from `append()` crashes instead of being caught. + +- [ ] **Step 3: Wrap the _ondata handler in try/catch** + +Change lines 26-29 of `src/server/stdio.ts` from: + +```typescript + _ondata = (chunk: Buffer) => { + this._readBuffer.append(chunk); + this.processReadBuffer(); + }; +``` + +to: + +```typescript + _ondata = (chunk: Buffer) => { + try { + this._readBuffer.append(chunk); + this.processReadBuffer(); + } catch (error) { + this.onerror?.(error as Error); + this.close().catch(() => {}); + } + }; +``` + +- [ ] **Step 4: Run tests to confirm they pass** + +Run: `npx vitest test/server/stdio.test.ts --run` +Expected: All tests PASS. + +- [ ] **Step 5: Suggest commit** + +```bash +git add src/server/stdio.ts test/server/stdio.test.ts +git commit -m "fix(server): catch ReadBuffer overflow in StdioServerTransport + +Prevents an uncaught exception when ReadBuffer.append() throws due to +exceeding the max buffer size. Routes the error to onerror and closes +the transport. + +Ref: GHSA-wqgc-pwpr-pq7r" +``` + +--- + +### Task 3: Add try/catch to StdioClientTransport data handler + +**Files:** +- Modify: `src/client/stdio.ts:150-153` +- Modify: `test/client/stdio.test.ts` + +- [ ] **Step 1: Add overflow test for StdioClientTransport** + +Append the following test to `test/client/stdio.test.ts`: + +```typescript +test('should fire onerror and close when ReadBuffer overflows', async () => { + const client = new StdioClientTransport({ + command: 'node', + args: ['-e', 'process.stdout.write(Buffer.alloc(11 * 1024 * 1024, 0x41))'] + }); + + const errorReceived = new Promise(resolve => { + client.onerror = resolve; + }); + const closed = new Promise(resolve => { + client.onclose = () => resolve(); + }); + + await client.start(); + + const error = await errorReceived; + expect(error.message).toMatch(/ReadBuffer exceeded maximum size/); + await closed; +}); +``` + +- [ ] **Step 2: Run to confirm the test fails** + +Run: `npx vitest test/client/stdio.test.ts --run` +Expected: FAIL — the uncaught throw from `append()` crashes. + +- [ ] **Step 3: Wrap the stdout data handler in try/catch** + +Change lines 150-153 of `src/client/stdio.ts` from: + +```typescript + this._process.stdout?.on('data', chunk => { + this._readBuffer.append(chunk); + this.processReadBuffer(); + }); +``` + +to: + +```typescript + this._process.stdout?.on('data', chunk => { + try { + this._readBuffer.append(chunk); + this.processReadBuffer(); + } catch (error) { + this.onerror?.(error as Error); + this.close().catch(() => {}); + } + }); +``` + +- [ ] **Step 4: Run tests to confirm they pass** + +Run: `npx vitest test/client/stdio.test.ts --run` +Expected: All tests PASS. + +- [ ] **Step 5: Suggest commit** + +```bash +git add src/client/stdio.ts test/client/stdio.test.ts +git commit -m "fix(client): catch ReadBuffer overflow in StdioClientTransport + +Prevents an uncaught exception when ReadBuffer.append() throws due to +exceeding the max buffer size. Routes the error to onerror and closes +the transport. + +Ref: GHSA-wqgc-pwpr-pq7r" +``` + +--- + +### Task 4: Full verification + +- [ ] **Step 1: Run typecheck** + +Run: `npm run typecheck` +Expected: No errors. + +- [ ] **Step 2: Run full test suite** + +Run: `npm test` +Expected: All tests PASS. + +- [ ] **Step 3: Run lint** + +Run: `npm run lint` +Expected: No errors (or fix with `npm run lint:fix`). diff --git a/docs/superpowers/plans/2026-06-23-sdk-shared-package.md b/docs/superpowers/plans/2026-06-23-sdk-shared-package.md new file mode 100644 index 0000000000..c148328705 --- /dev/null +++ b/docs/superpowers/plans/2026-06-23-sdk-shared-package.md @@ -0,0 +1,716 @@ +# `@modelcontextprotocol/sdk-shared` Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Extract the canonical MCP spec data model (Zod schemas + derived TS types + protocol constants) into a new publishable package `@modelcontextprotocol/sdk-shared`, so v1→v2 schema-validation migration becomes a mechanical import-path swap with `.parse`/`.safeParse`/all Zod methods preserved. + +**Architecture:** A new zod-only, runtime-neutral package owns `constants.ts` + `schemas.ts` + `types.ts` (moved from `core`). `core` keeps thin re-export shims at the old paths (churn control); `core/public`, `server`, and `client` re-export the **types** (Zod-free) and continue to expose `specTypeSchemas` unchanged; the raw Zod `*Schema` constants are reachable only from `sdk-shared`. The codemod routes `@modelcontextprotocol/sdk/types.js` → `@modelcontextprotocol/sdk-shared` as a fixed path swap and drops the `specSchemaAccess` rewriting entirely. + +**Tech Stack:** TypeScript (NodeNext, `tsgo` typecheck), Zod v4, tsdown (build, ESM `.mjs`/`.d.mts`), vitest, ts-morph (codemod), changesets (prerelease `alpha` mode), pnpm workspaces. + +## Global Constraints + +- Node engine floor: `>=20`. Package version line: `2.0.0-alpha.2` (match other runtime packages). +- Formatting (Prettier, `.prettierrc.json`): 4-space indent, single quotes, semicolons, **no trailing commas**, print width 140. All new/edited files must satisfy `prettier --check`. +- Source imports use explicit `.js` extensions (NodeNext); sibling `.ts` files import each other as `./x.js`. +- Public API uses **explicit named exports** except `types.ts`, which is the one intentional `export *` (it contains only spec-derived TS types). +- `sdk-shared` must be **runtime-neutral** (no Node builtins) — guarded by a `barrelClean` test. +- `sdk-shared`'s only runtime dependency is `zod` (`catalog:runtimeShared` → `^4.2.0`). No `publishConfig` (root `.npmrc` + changesets `access: public` handle it). +- Never run `git add`/`git commit` (a hook blocks it). At each "Commit" step, **print the suggested commands** for the user to run manually. +- Typecheck per package: `tsgo -p tsconfig.json --noEmit`. Tests: `vitest run` (tests live in `test/**/*.test.ts`, not colocated). + +--- + +## File Structure + +**New package `packages/sdk-shared/`:** +- `package.json`, `tsconfig.json`, `tsdown.config.ts`, `vitest.config.js`, `eslint.config.mjs`, `README.md` +- `src/constants.ts`, `src/schemas.ts`, `src/types.ts` — relocated from `packages/core/src/types/` +- `src/index.ts` — main barrel: types + constants + schemas (everything; first-class Zod) +- `test/barrelClean.test.ts` — runtime-neutrality guard +- The `./types` subpath is served directly by the built `src/types.ts` (types-only; Zod-free) for `core/public` to re-export. + +**Modified in `packages/core/`:** +- `src/types/constants.ts`, `src/types/schemas.ts`, `src/types/types.ts` → become 1-line re-export shims pointing at `sdk-shared` (churn control) +- `src/exports/public/index.ts` → re-point the types `export *` and the constants named-export at `sdk-shared` +- `package.json` → add `@modelcontextprotocol/sdk-shared` dependency +- `src/types/specTypeSchema.ts` → its `import * as schemas from './schemas.js'` keeps working via the shim (no edit needed if shim is in place) + +**Modified in `packages/server/`, `packages/client/`:** +- `package.json` → add `@modelcontextprotocol/sdk-shared` dependency +- `tsdown.config.ts` → add `@modelcontextprotocol/sdk-shared` to `external`; add its `src` path to the dts `paths` so `.d.mts` resolves + +**Modified in `packages/codemod/`:** +- `scripts/generateVersions.ts` → add `sdk-shared` to `PACKAGE_DIRS`; regenerate `src/generated/versions.ts` +- `src/migrations/v1-to-v2/mappings/importMap.ts` → `sdk/types.js` target becomes `@modelcontextprotocol/sdk-shared` +- `src/migrations/v1-to-v2/transforms/index.ts` → remove `specSchemaAccess` from the pipeline +- delete `src/migrations/v1-to-v2/transforms/specSchemaAccess.ts` + `test/v1-to-v2/transforms/specSchemaAccess.test.ts` +- update `test/v1-to-v2/transforms/importPaths.test.ts` and any integration test expecting `specTypeSchemas` output +- `src/bin/batchTest.ts` → add `sdk-shared` to `LOCAL_PACKAGE_DIRS`; add `overrides` so transitive `server→sdk-shared` resolves to the local tarball + +**Modified docs / release:** +- `docs/migration.md`, `docs/migration-SKILL.md` → rewrite spec-schema validation section +- `.changeset/pre.json` → add `sdk-shared` to `initialVersions`; new `.changeset/add-sdk-shared-package.md` + +--- + +## Phase 1 — Create `sdk-shared`, move the spec data model, rewire consumers + +### Task 1.1: Scaffold the empty `sdk-shared` package + +**Files:** +- Create: `packages/sdk-shared/package.json`, `tsconfig.json`, `tsdown.config.ts`, `vitest.config.js`, `eslint.config.mjs`, `README.md`, `src/index.ts` +- Modify: `.changeset/pre.json` +- Create: `.changeset/add-sdk-shared-package.md` + +**Interfaces:** +- Produces: a buildable workspace package `@modelcontextprotocol/sdk-shared` whose `dist/index.mjs` + `dist/index.d.mts` exist. No real exports yet (placeholder). + +- [ ] **Step 1: Create `packages/sdk-shared/package.json`** + +```json +{ + "name": "@modelcontextprotocol/sdk-shared", + "private": false, + "version": "2.0.0-alpha.2", + "description": "Shared types and Zod schemas for the Model Context Protocol TypeScript SDK", + "license": "MIT", + "author": "Anthropic, PBC (https://anthropic.com)", + "homepage": "https://modelcontextprotocol.io", + "bugs": "https://github.com/modelcontextprotocol/typescript-sdk/issues", + "type": "module", + "repository": { + "type": "git", + "url": "git+https://github.com/modelcontextprotocol/typescript-sdk.git" + }, + "engines": { + "node": ">=20" + }, + "keywords": ["modelcontextprotocol", "mcp", "schemas", "types"], + "exports": { + ".": { + "types": "./dist/index.d.mts", + "import": "./dist/index.mjs" + }, + "./types": { + "types": "./dist/types.d.mts", + "import": "./dist/types.mjs" + } + }, + "types": "./dist/index.d.mts", + "typesVersions": { + "*": { + "types": ["dist/types.d.mts"] + } + }, + "files": ["dist"], + "scripts": { + "typecheck": "tsgo -p tsconfig.json --noEmit", + "build": "tsdown", + "build:watch": "tsdown --watch", + "prepack": "pnpm run build", + "lint": "eslint src/ && prettier --ignore-path ../../.prettierignore --check .", + "lint:fix": "eslint src/ --fix && prettier --ignore-path ../../.prettierignore --write .", + "check": "pnpm run typecheck && pnpm run lint", + "test": "vitest run", + "test:watch": "vitest" + }, + "dependencies": { + "zod": "catalog:runtimeShared" + }, + "devDependencies": { + "@modelcontextprotocol/eslint-config": "workspace:^", + "@modelcontextprotocol/tsconfig": "workspace:^", + "@modelcontextprotocol/vitest-config": "workspace:^", + "@eslint/js": "catalog:devTools", + "@typescript/native-preview": "catalog:devTools", + "eslint": "catalog:devTools", + "eslint-config-prettier": "catalog:devTools", + "eslint-plugin-n": "catalog:devTools", + "prettier": "catalog:devTools", + "tsdown": "catalog:devTools", + "typescript": "catalog:devTools", + "typescript-eslint": "catalog:devTools", + "vitest": "catalog:devTools" + } +} +``` + +- [ ] **Step 2: Create `packages/sdk-shared/tsconfig.json`** + +```json +{ + "extends": "@modelcontextprotocol/tsconfig", + "include": ["./"], + "exclude": ["node_modules", "dist"], + "compilerOptions": { + "paths": { "*": ["./*"] } + } +} +``` + +- [ ] **Step 3: Create `packages/sdk-shared/tsdown.config.ts`** (two entries: main + the types-only subpath) + +```ts +import { defineConfig } from 'tsdown'; + +export default defineConfig({ + failOnWarn: 'ci-only', + entry: ['src/index.ts', 'src/types.ts'], + format: ['esm'], + outDir: 'dist', + clean: true, + sourcemap: true, + target: 'esnext', + platform: 'node', + shims: true, + dts: { resolver: 'tsc' } +}); +``` + +- [ ] **Step 4: Create `packages/sdk-shared/vitest.config.js`** + +```js +import baseConfig from '@modelcontextprotocol/vitest-config'; + +export default baseConfig; +``` + +- [ ] **Step 5: Create `packages/sdk-shared/eslint.config.mjs`** + +```js +// @ts-check + +import baseConfig from '@modelcontextprotocol/eslint-config'; + +export default [ + ...baseConfig, + { + settings: { + 'import/internal-regex': '^@modelcontextprotocol/sdk-shared' + } + } +]; +``` + +- [ ] **Step 6: Create `packages/sdk-shared/README.md`** + +```md +# @modelcontextprotocol/sdk-shared + +Shared types and Zod schemas for the Model Context Protocol TypeScript SDK. Exposes the canonical MCP spec data model: the Zod `*Schema` constants, their derived TypeScript types, and protocol constants. + +- Import types and Zod schemas from `@modelcontextprotocol/sdk-shared`. +- For library-agnostic (Standard Schema) validation, prefer `specTypeSchemas` from `@modelcontextprotocol/server` / `@modelcontextprotocol/client`. +``` + +- [ ] **Step 7: Create placeholder `packages/sdk-shared/src/index.ts`** + +```ts +// Placeholder — real exports added in Task 1.2. +export const SDK_SHARED_PLACEHOLDER = true; +``` + +- [ ] **Step 8: Register the package in changesets prerelease state** — edit `.changeset/pre.json`, adding this entry to the `initialVersions` object (alphabetical position is fine): + +```json +"@modelcontextprotocol/sdk-shared": "2.0.0-alpha.0" +``` + +- [ ] **Step 9: Create `.changeset/add-sdk-shared-package.md`** + +```md +--- +'@modelcontextprotocol/sdk-shared': minor +--- + +Add @modelcontextprotocol/sdk-shared package: the canonical home for MCP spec Zod schemas, their derived TypeScript types, and protocol constants. +``` + +- [ ] **Step 10: Install + build to verify the scaffold** + +Run: `pnpm install && pnpm --filter @modelcontextprotocol/sdk-shared build` +Expected: install succeeds; build writes `packages/sdk-shared/dist/index.mjs` and `dist/index.d.mts` (and `dist/types.*`). Verify: `ls packages/sdk-shared/dist` shows `index.mjs index.d.mts types.mjs types.d.mts`. + +- [ ] **Step 11: Commit** (print for the user) + +```bash +git add packages/sdk-shared .changeset/pre.json .changeset/add-sdk-shared-package.md +git commit -m "feat(sdk-shared): scaffold empty @modelcontextprotocol/sdk-shared package" +``` + +--- + +### Task 1.2: Relocate the spec data model into `sdk-shared` + +**Files:** +- Move: `packages/core/src/types/constants.ts` → `packages/sdk-shared/src/constants.ts` +- Move: `packages/core/src/types/schemas.ts` → `packages/sdk-shared/src/schemas.ts` +- Move: `packages/core/src/types/types.ts` → `packages/sdk-shared/src/types.ts` +- Modify: `packages/sdk-shared/src/index.ts` + +**Interfaces:** +- Produces: `@modelcontextprotocol/sdk-shared` exports all spec types + all `*Schema` Zod constants + all protocol constants from `.`; `@modelcontextprotocol/sdk-shared/types` exports the spec **types only**. +- Consumes: nothing new (the three files are self-contained — only external import is `zod/v4`). + +- [ ] **Step 1: Move the three files** (preserves content + history) + +```bash +git mv packages/core/src/types/constants.ts packages/sdk-shared/src/constants.ts +git mv packages/core/src/types/schemas.ts packages/sdk-shared/src/schemas.ts +git mv packages/core/src/types/types.ts packages/sdk-shared/src/types.ts +``` + +The internal relative imports between these three files (`./constants.js`, `./types.js`, `./schemas.js`) and `zod/v4` remain valid in the new location — no edits needed inside them. Remove the `⚠️ PUBLIC API` comment header in `types.ts` that references `exports/public/index.ts` only if it is now inaccurate; otherwise leave it. + +- [ ] **Step 2: Write the real `packages/sdk-shared/src/index.ts`** (replace the placeholder) + +```ts +// Canonical MCP spec data model: protocol constants, spec-derived TS types, and the +// Zod *Schema constants. The `.` entry is the first-class public surface (Zod included). +// The types-only `./types` subpath is served by ./types.ts directly (see package.json exports). +export * from './constants.js'; +export * from './types.js'; +export * from './schemas.js'; +``` + +- [ ] **Step 3: Typecheck `sdk-shared` in isolation** + +Run: `pnpm --filter @modelcontextprotocol/sdk-shared typecheck` +Expected: PASS (no errors). If `tsgo` reports a missing import, it means a fourth file was part of the closure — re-check `schemas.ts`/`types.ts`/`constants.ts` imports and move any additional self-contained spec file. + +- [ ] **Step 4: Build `sdk-shared`** + +Run: `pnpm --filter @modelcontextprotocol/sdk-shared build` +Expected: PASS; `dist/index.mjs` now contains the schema runtime values; `dist/types.d.mts` exposes the 178 types. + +- [ ] **Step 5: Commit** (print for the user) + +```bash +git add packages/sdk-shared packages/core/src/types +git commit -m "feat(sdk-shared): move spec constants, schemas, and types into sdk-shared" +``` + +--- + +### Task 1.3: Rewire `core` to consume `sdk-shared` via re-export shims + +**Files:** +- Create (at the old paths): `packages/core/src/types/constants.ts`, `packages/core/src/types/schemas.ts`, `packages/core/src/types/types.ts` — now 1-line re-export shims +- Modify: `packages/core/package.json` (add dependency) +- Modify: `packages/core/src/exports/public/index.ts` (re-point types `export *`) +- Modify: `packages/core/tsconfig.json` (path mapping for `tsgo`, if needed) + +**Interfaces:** +- Consumes: `@modelcontextprotocol/sdk-shared` (`.` and `./types`). +- Produces: `core`'s internal relative imports of `./types.js`/`./schemas.js`/`./constants.js` keep resolving (via shims); `core/public` exports the same public symbols as before (types via `sdk-shared/types`, constants via `sdk-shared`, no schema values), so `server`/`client` surfaces are unchanged and Zod-free. + +- [ ] **Step 1: Add the dependency to `packages/core/package.json`** — add to `dependencies`: + +```json +"@modelcontextprotocol/sdk-shared": "workspace:^" +``` + +- [ ] **Step 2: Create the re-export shims at the old core paths.** `packages/core/src/types/constants.ts`: + +```ts +// Moved to @modelcontextprotocol/sdk-shared. Re-exported here so core's internal +// relative imports (./constants.js) keep resolving without a wide rename. +export * from '@modelcontextprotocol/sdk-shared'; +``` + +`packages/core/src/types/schemas.ts`: + +```ts +// Moved to @modelcontextprotocol/sdk-shared. +export * from '@modelcontextprotocol/sdk-shared'; +``` + +`packages/core/src/types/types.ts`: + +```ts +// Moved to @modelcontextprotocol/sdk-shared (types-only subpath keeps this Zod-free). +export * from '@modelcontextprotocol/sdk-shared/types'; +``` + +(The `schemas.ts` shim re-exports the full surface so `import * as schemas from './schemas.js'` in `specTypeSchema.ts` still finds every `*Schema` value. The `types.ts` shim uses the types-only subpath so anything `export *`-ing it stays Zod-free.) + +- [ ] **Step 3: Re-point the types `export *` in `packages/core/src/exports/public/index.ts`.** The line currently reads `export * from '../../types/types.js';`. It can stay as-is (the shim now forwards to `sdk-shared/types`). **Verify** the constants named-export block (`export { BAGGAGE_META_KEY, … } from '../../types/constants.js';`) still resolves through the `constants.ts` shim. No code change required if shims are in place — confirm in Step 5. + +- [ ] **Step 4: Update `core`'s tsgo path mapping if needed.** If Step 5 typecheck fails to resolve `@modelcontextprotocol/sdk-shared`, add to `packages/core/tsconfig.json` `compilerOptions.paths`: + +```json +"@modelcontextprotocol/sdk-shared": ["./node_modules/@modelcontextprotocol/sdk-shared/src/index.ts"], +"@modelcontextprotocol/sdk-shared/types": ["./node_modules/@modelcontextprotocol/sdk-shared/src/types.ts"] +``` + +- [ ] **Step 5: Reinstall, typecheck, and test core** + +Run: `pnpm install && pnpm --filter @modelcontextprotocol/core typecheck && pnpm --filter @modelcontextprotocol/core test` +Expected: typecheck PASS; all core tests PASS. The key assertion: `specTypeSchemas` still builds (it reads schema values through the `schemas.ts` shim). + +- [ ] **Step 6: Commit** (print for the user) + +```bash +git add packages/core +git commit -m "refactor(core): consume sdk-shared via re-export shims; keep public surface unchanged" +``` + +--- + +### Task 1.4: Wire `server` and `client` to depend on `sdk-shared` (external, not bundled) + +**Files:** +- Modify: `packages/server/package.json`, `packages/client/package.json` (add dependency) +- Modify: `packages/server/tsdown.config.ts`, `packages/client/tsdown.config.ts` (external + dts paths) +- Modify: `packages/server/tsconfig.json`, `packages/client/tsconfig.json` (tsgo path mapping) + +**Interfaces:** +- Consumes: `@modelcontextprotocol/sdk-shared` at runtime (external dependency). +- Produces: `server`/`client` `dist` no longer inlines the schema/type source; their root barrels still re-export the spec **types** and `specTypeSchemas` (Zod-free); `barrelClean` still passes. + +- [ ] **Step 1: Add the dependency** to both `packages/server/package.json` and `packages/client/package.json` `dependencies`: + +```json +"@modelcontextprotocol/sdk-shared": "workspace:^" +``` + +- [ ] **Step 2: Mark it external in `packages/server/tsdown.config.ts`.** Add `'@modelcontextprotocol/sdk-shared'` to the `external` array (create the array if absent — server already has `external: ['@modelcontextprotocol/server/_shims']`): + +```ts + external: ['@modelcontextprotocol/server/_shims', '@modelcontextprotocol/sdk-shared'], +``` + +Add its source path to the dts `compilerOptions.paths` block so `.d.mts` generation resolves the external types: + +```ts + '@modelcontextprotocol/sdk-shared': ['../sdk-shared/src/index.ts'], + '@modelcontextprotocol/sdk-shared/types': ['../sdk-shared/src/types.ts'], +``` + +- [ ] **Step 3: Do the same in `packages/client/tsdown.config.ts`** (add `'@modelcontextprotocol/sdk-shared'` to `external`, and the two `paths` entries to the dts block). + +- [ ] **Step 4: Add tsgo path mapping** to `packages/server/tsconfig.json` and `packages/client/tsconfig.json` `compilerOptions.paths` (mirroring how they map `@modelcontextprotocol/core`): + +```json +"@modelcontextprotocol/sdk-shared": ["./node_modules/@modelcontextprotocol/sdk-shared/src/index.ts"], +"@modelcontextprotocol/sdk-shared/types": ["./node_modules/@modelcontextprotocol/sdk-shared/src/types.ts"] +``` + +- [ ] **Step 5: Reinstall, build, typecheck, test both packages** + +Run: `pnpm install && pnpm --filter @modelcontextprotocol/server --filter @modelcontextprotocol/client build && pnpm --filter @modelcontextprotocol/server --filter @modelcontextprotocol/client typecheck && pnpm --filter @modelcontextprotocol/server --filter @modelcontextprotocol/client test` +Expected: all PASS, including `barrelClean.test.ts`. + +- [ ] **Step 6: Verify `sdk-shared` is external in the build output** (not inlined) + +Run: `grep -c "@modelcontextprotocol/sdk-shared" packages/server/dist/index.mjs` +Expected: ≥ 1 (an `import ... from "@modelcontextprotocol/sdk-shared..."` line — proving it's referenced as an external dependency, not bundled). And the spec schema source is NOT inlined: `grep -c "z.object" packages/server/dist/index.mjs` should be markedly lower than before the change (spot-check; not a hard gate). + +- [ ] **Step 7: Full repo gate** + +Run: `pnpm typecheck:all && pnpm test:all` +Expected: all PASS. This confirms the move didn't break any sibling package. + +- [ ] **Step 8: Commit** (print for the user) + +```bash +git add packages/server packages/client +git commit -m "refactor(server,client): depend on sdk-shared as an external dependency" +``` + +--- + +## Phase 2 — Codemod: route `types.js` → `sdk-shared`, drop `specSchemaAccess` + +### Task 2.1: Register `sdk-shared` in the codemod version map + +**Files:** +- Modify: `packages/codemod/scripts/generateVersions.ts` +- Regenerate: `packages/codemod/src/generated/versions.ts` + +**Interfaces:** +- Produces: `V2_PACKAGE_VERSIONS` includes `@modelcontextprotocol/sdk-shared`, so `updatePackageJson` is allowed to add it to a consumer's deps. + +- [ ] **Step 1: Add `sdk-shared` to `PACKAGE_DIRS`** in `packages/codemod/scripts/generateVersions.ts`: + +```ts +const PACKAGE_DIRS: Record = { + '@modelcontextprotocol/client': 'client', + '@modelcontextprotocol/server': 'server', + '@modelcontextprotocol/node': 'middleware/node', + '@modelcontextprotocol/express': 'middleware/express', + '@modelcontextprotocol/server-legacy': 'server-legacy', + '@modelcontextprotocol/sdk-shared': 'sdk-shared' +}; +``` + +- [ ] **Step 2: Regenerate** + +Run: `pnpm --filter @modelcontextprotocol/codemod generate:versions` +Expected: `src/generated/versions.ts` now contains `'@modelcontextprotocol/sdk-shared': '^2.0.0-alpha.2'`. Verify: `grep sdk-shared packages/codemod/src/generated/versions.ts`. + +- [ ] **Step 3: Commit** (print for the user) + +```bash +git add packages/codemod/scripts/generateVersions.ts packages/codemod/src/generated/versions.ts +git commit -m "feat(codemod): register sdk-shared in V2_PACKAGE_VERSIONS" +``` + +--- + +### Task 2.2: Route `sdk/types.js` to `sdk-shared` (TDD) + +**Files:** +- Test: `packages/codemod/test/v1-to-v2/transforms/importPaths.test.ts` +- Modify: `packages/codemod/src/migrations/v1-to-v2/mappings/importMap.ts` + +**Interfaces:** +- Consumes: `lookupImportMapping` (already extension-tolerant from prior work). +- Produces: any import from `@modelcontextprotocol/sdk/types.js` or `@modelcontextprotocol/sdk/types` is rewritten to `@modelcontextprotocol/sdk-shared` (fixed target, no context resolution), names preserved, and `@modelcontextprotocol/sdk-shared` is added to `usedPackages`. + +- [ ] **Step 1: Write the failing test** — add to `importPaths.test.ts` inside the `describe('import-paths transform', …)` block. Also covers that schema-value imports keep their names (no `specTypeSchemas` rewrite): + +```ts +it('routes sdk/types.js to @modelcontextprotocol/sdk-shared (types + schemas, fixed target)', () => { + const input = [ + `import { CallToolResult, CallToolResultSchema } from '@modelcontextprotocol/sdk/types.js';`, + '' + ].join('\n'); + const project = new Project({ useInMemoryFileSystem: true }); + const sourceFile = project.createSourceFile('test.ts', input); + const result = importPathsTransform.apply(sourceFile, { projectType: 'server' }); + const output = sourceFile.getFullText(); + expect(output).toContain(`from "@modelcontextprotocol/sdk-shared"`); + expect(output).toContain('CallToolResult'); + expect(output).toContain('CallToolResultSchema'); + expect(output).not.toContain('@modelcontextprotocol/sdk/types'); + expect(output).not.toContain('specTypeSchemas'); + expect(result.usedPackages?.has('@modelcontextprotocol/sdk-shared')).toBe(true); +}); +``` + +- [ ] **Step 2: Run it; verify it fails** + +Run: `pnpm --filter @modelcontextprotocol/codemod test -- importPaths -t "sdk-shared (types + schemas"` +Expected: FAIL — current output routes to `@modelcontextprotocol/server` (RESOLVE_BY_CONTEXT), so the `sdk-shared` assertion fails. + +- [ ] **Step 3: Change the `types.js` mapping** in `packages/codemod/src/migrations/v1-to-v2/mappings/importMap.ts`. Replace the existing entry: + +```ts + '@modelcontextprotocol/sdk/types.js': { + target: '@modelcontextprotocol/sdk-shared', + status: 'moved', + renamedSymbols: { + ResourceTemplate: 'ResourceTemplateType' + } + }, +``` + +(Only this entry changes from `RESOLVE_BY_CONTEXT` to the fixed `@modelcontextprotocol/sdk-shared` target. Leave `shared/protocol.js`, `shared/transport.js`, `inMemory.js`, etc. as `RESOLVE_BY_CONTEXT`.) + +- [ ] **Step 4: Run the test; verify it passes** + +Run: `pnpm --filter @modelcontextprotocol/codemod test -- importPaths -t "sdk-shared (types + schemas"` +Expected: PASS. + +- [ ] **Step 5: Update the now-obsolete `types.js` context tests.** The existing tests `resolves sdk/types.js based on sibling client imports`, `resolves sdk/types.js based on sibling server imports`, and the extensionless `resolves extensionless sdk/types …` tests now expect `@modelcontextprotocol/sdk-shared` instead of `@modelcontextprotocol/client`/`/server`. Update each assertion to `expect(result).toContain('@modelcontextprotocol/sdk-shared')` (drop the client/server expectations for the `types`-only cases). Re-run the full file: + +Run: `pnpm --filter @modelcontextprotocol/codemod test -- importPaths` +Expected: all PASS. + +- [ ] **Step 6: Commit** (print for the user) + +```bash +git add packages/codemod/src/migrations/v1-to-v2/mappings/importMap.ts packages/codemod/test/v1-to-v2/transforms/importPaths.test.ts +git commit -m "feat(codemod): route sdk/types.js to @modelcontextprotocol/sdk-shared" +``` + +--- + +### Task 2.3: Remove the `specSchemaAccess` transform + +**Files:** +- Modify: `packages/codemod/src/migrations/v1-to-v2/transforms/index.ts` +- Delete: `packages/codemod/src/migrations/v1-to-v2/transforms/specSchemaAccess.ts` +- Delete: `packages/codemod/test/v1-to-v2/transforms/specSchemaAccess.test.ts` +- Modify: codemod integration tests that assert `specTypeSchemas` output (e.g. `test/integration.test.ts`) + +**Interfaces:** +- Produces: `*Schema` value usages (`.parse`, `.safeParse`, `.extend`, …) pass through untouched — they ride the `types.js → sdk-shared` path swap with names intact. + +- [ ] **Step 1: Write a failing pass-through test** in `importPaths.test.ts` (or a new `test/v1-to-v2/passthrough.test.ts` running the full migration) asserting `.parse()` survives. Minimal transform-level version: + +```ts +it('leaves *Schema runtime usage (.parse) untouched after routing to sdk-shared', () => { + const input = [ + `import { CallToolResultSchema } from '@modelcontextprotocol/sdk/types.js';`, + `const x = CallToolResultSchema.parse(value);`, + '' + ].join('\n'); + const project = new Project({ useInMemoryFileSystem: true }); + const sourceFile = project.createSourceFile('test.ts', input); + importPathsTransform.apply(sourceFile, { projectType: 'server' }); + const output = sourceFile.getFullText(); + expect(output).toContain('CallToolResultSchema.parse(value)'); + expect(output).not.toContain('specTypeSchemas'); + expect(output).not.toContain("['~standard']"); +}); +``` + +- [ ] **Step 2: Run it; verify current behavior** — with `specSchemaAccess` still in the pipeline this transform-only test on `importPathsTransform` already passes (specSchemaAccess is a separate transform). To see the regression the removal prevents, run the FULL migration in this test instead by importing and applying every transform in order. Confirm that BEFORE removal the full-migration output contains `specTypeSchemas` (FAIL of the `not.toContain` assertion), proving `specSchemaAccess` is what rewrites it. + +Run: `pnpm --filter @modelcontextprotocol/codemod test -- passthrough` +Expected: FAIL on `not.toContain('specTypeSchemas')` (full-migration variant). + +- [ ] **Step 3: Remove `specSchemaAccess` from the pipeline** in `packages/codemod/src/migrations/v1-to-v2/transforms/index.ts` — delete its import and its entry in the exported transforms array. + +- [ ] **Step 4: Delete the transform + its unit test** + +```bash +git rm packages/codemod/src/migrations/v1-to-v2/transforms/specSchemaAccess.ts +git rm packages/codemod/test/v1-to-v2/transforms/specSchemaAccess.test.ts +``` + +- [ ] **Step 5: Update integration tests.** Search for residual expectations and fix them: + +Run: `grep -rn "specTypeSchemas\|~standard\|specSchemaAccess" packages/codemod/test packages/codemod/src/migrations` +Expected after fixes: only legitimate references remain (none asserting the codemod *produces* `specTypeSchemas`). Update `test/integration.test.ts` cases that expected `.parse`→`validate` rewrites to instead expect the schema usage unchanged. + +- [ ] **Step 6: Run the full codemod suite** + +Run: `pnpm --filter @modelcontextprotocol/codemod test` +Expected: all PASS. + +- [ ] **Step 7: Typecheck + lint the codemod** (catches the dangling `specSchemaAccess` import and any unused `specSchemaMap` reference) + +Run: `pnpm --filter @modelcontextprotocol/codemod check` +Expected: PASS. If `src/generated/specSchemaMap.ts` / `scripts/generateSpecSchemaMap.ts` are now unused, remove them and the `generate:spec-schemas` prebuild step; otherwise leave them. + +- [ ] **Step 8: Commit** (print for the user) + +```bash +git add packages/codemod +git commit -m "refactor(codemod): drop specSchemaAccess; schema usage migrates by path swap" +``` + +--- + +## Phase 3 — Batch-test validation + docs + +### Task 3.1: Teach the batch test about `sdk-shared` and re-validate firebase-tools + +**Files:** +- Modify: `packages/codemod/src/bin/batchTest.ts` + +**Interfaces:** +- Consumes: the packed local tarballs. +- Produces: the batch test packs `sdk-shared` and forces the transitive `server`/`client` → `sdk-shared` edge to resolve to the local tarball. + +- [ ] **Step 1: Add `sdk-shared` to `LOCAL_PACKAGE_DIRS`** in `packages/codemod/src/bin/batchTest.ts`: + +```ts + '@modelcontextprotocol/sdk-shared': path.join(SDK_ROOT, 'packages/sdk-shared'), +``` + +- [ ] **Step 2: Force transitive resolution via `overrides`.** In `rewriteToLocalTarballs` (or right after it), ensure the consumer `package.json` gets an `overrides` map pinning `@modelcontextprotocol/sdk-shared` (and the other v2 packages) to their local tarball paths, so `server`'s own `^2.0.0-alpha.2` dependency on `sdk-shared` resolves locally. Add, after the dependency rewrite loop: + +```ts + // npm/pnpm: pin transitive @modelcontextprotocol/* (e.g. server -> sdk-shared) to local tarballs. + const overrides = (pkgJson.overrides as Record | undefined) ?? {}; + for (const [name, tarballPath] of Object.entries(tarballs)) { + overrides[name] = `file:${tarballPath}`; + } + pkgJson.overrides = overrides; + rewrites++; // ensure the file is written +``` + +(If the manifest's package manager is pnpm, the equivalent key is `pnpm.overrides`; firebase-tools uses npm, so top-level `overrides` is correct. Generalize only if a pnpm repo is added.) + +- [ ] **Step 3: Rebuild SDK packages and re-run the batch test** + +Run: `pnpm build:all && pnpm --filter @modelcontextprotocol/codemod batch-test` +Expected: completes; `packages/codemod/batch-test/results/summary.json` shows `firebase/firebase-tools` with `newErrors.typecheck: 0`. + +- [ ] **Step 4: Confirm the win in the report** + +Run: `node -e "const r=require('./packages/codemod/batch-test/results/firebase_firebase-tools/report.json');const p=r.packages[0];console.log('post typecheck exit:',p.postCodemod.typecheck.exitCode);console.log('Unknown SDK import path diags:',p.codemod.diagnostics.filter(d=>d.message.includes('Unknown SDK import path')).length);console.log('project-type diags:',p.codemod.diagnostics.filter(d=>d.message.includes('Could not determine project type')).length);"` +Expected: `post typecheck exit: 0`; `Unknown SDK import path diags: 0`; `project-type diags: 0`. Spot-check `repos/firebase_firebase-tools/src/mcp/onemcp/onemcp_server.ts` — the `.parse()` calls are intact and import `*Schema` from `@modelcontextprotocol/sdk-shared`. + +- [ ] **Step 5: Commit** (print for the user) + +```bash +git add packages/codemod/src/bin/batchTest.ts +git commit -m "test(codemod): pack sdk-shared and pin transitive deps in batch test" +``` + +--- + +### Task 3.2: Migration docs + finalize + +**Files:** +- Modify: `docs/migration.md`, `docs/migration-SKILL.md` + +**Interfaces:** none (docs only). + +- [ ] **Step 1: Rewrite the spec-schema validation section in `docs/migration.md`.** Replace the `CallToolResultSchema` → `specTypeSchemas.X['~standard'].validate()` guidance (around the section found by `grep -n "specTypeSchemas\|CallToolResultSchema" docs/migration.md`) with: + +```md +### Schema validation (`*Schema.parse` / `.safeParse`) + +The Zod schema constants moved to `@modelcontextprotocol/sdk-shared`. Update the import path; the schemas are unchanged Zod schemas, so `.parse()`, `.safeParse()`, `.extend()`, etc. keep working. + +```ts +// v1 +import { CallToolResultSchema } from '@modelcontextprotocol/sdk/types.js'; +// v2 +import { CallToolResultSchema } from '@modelcontextprotocol/sdk-shared'; + +const result = CallToolResultSchema.parse(value); // unchanged +``` + +For library-agnostic (Standard Schema) validation that does not couple your code to Zod, use `specTypeSchemas` from `@modelcontextprotocol/server` or `@modelcontextprotocol/client` instead: + +```ts +import { specTypeSchemas } from '@modelcontextprotocol/server'; +const r = specTypeSchemas.CallToolResult['~standard'].validate(value); // { value, issues } +``` +``` + +- [ ] **Step 2: Update `docs/migration-SKILL.md`** — replace the mapping-table rows that map `Schema.parse(value)` → `specTypeSchemas.['~standard'].validate(value)` with a row mapping the **import path**: `import … from '@modelcontextprotocol/sdk/types.js'` → `import … from '@modelcontextprotocol/sdk-shared'` (schemas and types), and note that `.parse`/`.safeParse` are unchanged. Keep the `specTypeSchemas` row as the optional library-agnostic alternative. + +- [ ] **Step 3: Sync snippets + docs check** + +Run: `pnpm sync:snippets && pnpm run docs:check` +Expected: PASS (or no changes). Fix any snippet drift. + +- [ ] **Step 4: Final full-repo gate** + +Run: `pnpm check:all && pnpm test:all` +Expected: all PASS. + +- [ ] **Step 5: Commit** (print for the user) + +```bash +git add docs/migration.md docs/migration-SKILL.md +git commit -m "docs(migration): schemas import from @modelcontextprotocol/sdk-shared" +``` + +--- + +## Self-Review + +**Spec coverage:** package creation (1.1), spec types + Zod schema move (1.2), first-class Zod positioning / no nudge (codemod has no nudge; docs present both — 2.2/2.3/3.2), regular `dependency` model (1.3/1.4), external-not-bundled (1.4), types-only re-export keeping the surface Zod-free (1.2 `./types` + 1.3 shim), `specTypeSchemas` unchanged (1.3), churn-limiting shims (1.3), codemod path swap (2.2), drop `specSchemaAccess` (2.3), batch-test wiring + 0-error validation (3.1), docs + changeset (1.1/3.2). PR #2277 supersession is covered by the `specSchemaAccess` removal (no `specTypeSchemas` rewrite produced). All spec sections map to a task. + +**Placeholder scan:** no `TBD`/`TODO`; the one conditional (`tsconfig paths` in 1.3 Step 4 / 1.4 Step 4) is gated on a concrete typecheck failure with the exact lines to add. Move tasks specify exact `git mv` targets rather than reproducing the 2346-line `schemas.ts` (relocation, not authoring). + +**Type/name consistency:** `@modelcontextprotocol/sdk-shared` used verbatim throughout; `./types` subpath defined in 1.1 (package.json exports + typesVersions), produced in 1.2 (built from `src/types.ts`), consumed in 1.3 (core `types.ts` shim) and 1.4 (server/client dts paths); `lookupImportMapping` (2.2) matches the existing helper; `LOCAL_PACKAGE_DIRS`/`rewriteToLocalTarballs`/`tarballs` (3.1) match `batchTest.ts`. + +## Execution Handoff + +Two execution options: + +1. **Subagent-Driven (recommended)** — a fresh subagent per task, with review between tasks. +2. **Inline Execution** — execute tasks in this session with checkpoints. + +Note: every "Commit" step prints commands for **you** to run (the `git add`/`git commit` hook blocks the agent from committing). diff --git a/docs/superpowers/specs/2026-05-11-codemod-batch-test-design.md b/docs/superpowers/specs/2026-05-11-codemod-batch-test-design.md new file mode 100644 index 0000000000..03709e94e2 --- /dev/null +++ b/docs/superpowers/specs/2026-05-11-codemod-batch-test-design.md @@ -0,0 +1,288 @@ +# Codemod Batch Test: Design Spec + +Repeatable process for running the MCP v1-to-v2 codemod against real-world repos, identifying issues, and iterating on the codemod. + +## Goal + +Improve the codemod by testing it against 10-15 curated external repos. Each iteration: run the codemod, compare baseline vs. post-codemod check results, have Claude categorize failures, fix the codemod, repeat. + +## System Overview + +Three components, all living in `packages/codemod/batch-test/`: + +1. **Repo manifest** (`repos.json`) -- JSON file listing target repos, their structure, and optional overrides. +2. **Batch runner** (`run-codemod-batch.sh`) -- Shell script that iterates the manifest: clones, installs, baselines, codemods, re-checks, writes structured output. +3. **Analysis prompt** (`analyze-prompt.md`) -- Instructions for Claude Code to run the script and analyze results in a single session. + +### Data Flow + +``` +repos.json --> run-codemod-batch.sh --> results//report.json (per-repo) + --> results/summary.json (consolidated) + +Claude Code: runs script, reads results, produces categorized analysis +``` + +## Repo Manifest (`repos.json`) + +An array of repo entries. Each entry represents a GitHub repo and one or more packages within it that use `@modelcontextprotocol/sdk` v1. + +```json +[ + { + "repo": "owner/repo-name", + "ref": "main", + "packages": [ + { + "dir": "packages/mcp-server", + "sourceDir": "src", + "checks": { + "typecheck": "npm run check:ts", + "test": null + } + } + ] + } +] +``` + +### Fields + +| Field | Required | Default | Description | +|-------|----------|---------|-------------| +| `repo` | yes | -- | GitHub `owner/name` | +| `ref` | no | `main` | Branch or tag to check out | +| `packages` | no | `[{ "dir": ".", "sourceDir": "src" }]` | Package targets within the repo | +| `packages[].dir` | yes | -- | Path to the package root (where its `package.json` lives) | +| `packages[].sourceDir` | no | `src` | Source directory relative to `dir` (passed to codemod) | +| `packages[].checks` | no | auto-detect | Override check commands; set a key to `null` to skip that check | + +### Auto-Detection Rules + +**Package manager** (first lockfile found at repo root): +- `pnpm-lock.yaml` -> `pnpm` +- `yarn.lock` -> `yarn` +- `package-lock.json` -> `npm` +- `bun.lockb` -> `bun` + +**Check commands** (read `scripts` from the package's `package.json`, first match wins): + +| Check | Script names probed (in order) | Fallback | +|-------|-------------------------------|----------| +| typecheck | `typecheck`, `type-check`, `check:types`, `tsc` | `npx tsc --noEmit` | +| build | `build`, `compile` | skip | +| test | `test`, `test:unit`, `test:all` | skip | +| lint | `lint`, `lint:check` | skip | + +The detected command runs as ` run `. + +## Batch Runner (`run-codemod-batch.sh`) + +### CLI + +```bash +./run-codemod-batch.sh [--manifest repos.json] [--output-dir ./results] [--clone-dir ./repos] [--fresh-clones] +``` + +| Flag | Default | Description | +|------|---------|-------------| +| `--manifest` | `./repos.json` | Path to repo manifest | +| `--output-dir` | `./results` | Where to write reports | +| `--clone-dir` | `./repos` | Where to clone repos | +| `--fresh-clones` | off | Force re-clone even if clone exists | + +Clones are kept between runs by default for fast iteration. + +### Per-Repo Flow + +``` +1. CLONE OR RESET + - If clone exists: git restore . && git clean -fd + - If no clone: git clone --depth 1 --branch + +2. DETECT PACKAGE MANAGER + - Check for lockfile at repo root + +3. INSTALL + - cd && install + - If install fails: record error, skip to next repo + +4. BASELINE CHECKS (for each package) + - Auto-detect or use override check commands + - Run: typecheck, build, test, lint + - Capture: exit code, stdout, stderr for each + +5. RUN CODEMOD (for each package) + - node /packages/codemod/dist/cli.mjs v1-to-v2 \ + // --verbose + - Capture: full output, diagnostics, change count + +6. RE-INSTALL + - cd && install + - Picks up new v2 deps from updated package.json files + +7. POST-CODEMOD CHECKS (for each package) + - Same checks as step 4, captured separately + +8. WRITE REPORT + - Write per-repo JSON to results//report.json + - Append entry to summary +``` + +### Error Handling + +If any step fails for a repo, the script logs the failure, writes what it has to the report, and moves to the next repo. One broken repo does not stop the batch. + +### Path Resolution + +The script resolves `SDK_ROOT` from its own location (`SDK_ROOT=$(cd "$(dirname "$0")/../../.." && pwd)`). All default paths (`--clone-dir`, `--output-dir`) are relative to the script's directory (`packages/codemod/batch-test/`). + +### Codemod Binary + +The script always uses the locally-built codemod from the current branch: +``` +node "$SDK_ROOT/packages/codemod/dist/cli.mjs" +``` +This ensures each run tests the current state of the codemod. + +## Output Format + +### Per-Repo Report (`results//report.json`) + +```json +{ + "repo": "user/mcp-server-example", + "ref": "main", + "timestamp": "2026-05-11T14:30:00Z", + "packageManager": "pnpm", + "packages": [ + { + "dir": ".", + "sourceDir": "src", + "codemod": { + "filesChanged": 12, + "totalChanges": 47, + "diagnostics": [ + { + "level": "warning", + "file": "src/server.ts", + "line": 42, + "message": "Destructuring pattern for 'extra' -- review manually", + "transformId": "context" + } + ] + }, + "baseline": { + "typecheck": { "exitCode": 0, "stdout": "", "stderr": "" }, + "build": { "exitCode": 0, "stdout": "", "stderr": "" }, + "test": { "exitCode": 0, "stdout": "", "stderr": "" }, + "lint": { "exitCode": 0, "stdout": "", "stderr": "" } + }, + "postCodemod": { + "typecheck": { "exitCode": 2, "stdout": "", "stderr": "src/handler.ts(15,3): error TS2345: ..." }, + "build": { "exitCode": 2, "stdout": "", "stderr": "..." }, + "test": { "exitCode": 0, "stdout": "", "stderr": "" }, + "lint": { "exitCode": 0, "stdout": "", "stderr": "" } + } + } + ] +} +``` + +### Consolidated Summary (`results/summary.json`) + +```json +{ + "timestamp": "2026-05-11T14:30:00Z", + "codemodVersion": "2.0.0-alpha.0", + "codemodCommit": "abc1234", + "totalRepos": 12, + "totalPackages": 15, + "results": [ + { + "repo": "user/mcp-server-example", + "package": ".", + "baselineClean": true, + "postCodemodClean": false, + "newErrors": { "typecheck": 3, "build": 1, "test": 0, "lint": 0 }, + "codemodDiagnostics": { "warning": 2, "error": 0, "info": 1 } + } + ], + "aggregated": { + "reposClean": 7, + "reposWithNewErrors": 5, + "totalNewTypecheckErrors": 18, + "totalCodemodWarnings": 12, + "topErrorPatterns": ["TS2345", "TS2339", "TS2554"] + } +} +``` + +## Claude Analysis Workflow + +### Prompt (`analyze-prompt.md`) + +Saved in `packages/codemod/batch-test/analyze-prompt.md`. You tell Claude Code to follow these instructions: + +``` +Run the batch codemod test and analyze results: + +1. Build the codemod: + pnpm --filter @modelcontextprotocol/codemod build + +2. Run the batch test: + ./packages/codemod/batch-test/run-codemod-batch.sh + +3. Read results/summary.json for the overview. + +4. For each repo with new errors, read its results//report.json. + +5. Categorize each new error (present in postCodemod but not in baseline): + - codemod-bug: The transform produced incorrect output + - missing-transform: The codemod should handle this pattern but doesn't + - manual-migration: Expected -- documented in migration guide, needs human judgment + - repo-specific: Unusual pattern unique to this repo, not worth handling + +6. Produce findings grouped by category with: + - Repo, file, line, error message + - Root cause (one sentence) + - For codemod-bug/missing-transform: which transform to fix and what correct output looks like + +7. Produce a "Priority Fixes" list: top 3-5 codemod improvements sorted by impact + (number of repos affected). +``` + +### Iteration Loop + +``` +1. Fix a codemod transform +2. Tell Claude: "Re-run the batch test and analyze" + --> Claude rebuilds codemod, resets clones, re-runs, reads results, analyzes +3. Review Claude's findings +4. Go to 1 +``` + +## Error Categorization Reference + +| Category | Meaning | Action | +|----------|---------|--------| +| `codemod-bug` | Transform produced wrong output | Fix the transform | +| `missing-transform` | Pattern not handled | Add handling to existing transform or create new one | +| `manual-migration` | Requires human judgment (removed API, architectural change) | Ensure migration guide covers it; improve codemod diagnostic | +| `repo-specific` | Unusual pattern unique to one repo | Document but don't add to codemod | + +## File Structure + +``` +packages/codemod/batch-test/ + repos.json # Repo manifest (curated list) + run-codemod-batch.sh # Batch runner script + analyze-prompt.md # Claude analysis instructions + repos/ # Cloned repos (gitignored) + results/ # Output reports (gitignored) + summary.json + / + report.json +``` + +`repos/` and `results/` are added to `.gitignore`. Only the manifest, script, and prompt are committed. diff --git a/docs/superpowers/specs/2026-06-02-readbuffer-max-size-design.md b/docs/superpowers/specs/2026-06-02-readbuffer-max-size-design.md new file mode 100644 index 0000000000..a8e89c3647 --- /dev/null +++ b/docs/superpowers/specs/2026-06-02-readbuffer-max-size-design.md @@ -0,0 +1,61 @@ +# ReadBuffer Maximum Size Guard + +**Date:** 2026-06-02 +**Advisory:** GHSA-wqgc-pwpr-pq7r +**Severity:** Low (DoS via stdio transport, local attack surface) + +## Problem + +`ReadBuffer.append()` in `packages/core/src/shared/stdio.ts` concatenates incoming data with no size limit. A malicious MCP server subprocess can write continuous data to stdout without newline delimiters, causing the host process (Claude Desktop, Cursor, VS Code, etc.) to grow memory without bound until OOM-killed. + +The `data` event handlers in both `StdioClientTransport` and `StdioServerTransport` call `append()` outside any try/catch, so a thrown error from `append()` would become an uncaught exception — this must also be addressed. + +## Design + +### 1. ReadBuffer (`packages/core/src/shared/stdio.ts`) + +- Add exported constant `DEFAULT_MAX_BUFFER_SIZE = 10 * 1024 * 1024` (10 MB). +- Constructor accepts optional `{ maxBufferSize?: number }` options object. +- `append()` checks `(currentSize + chunk.length) > maxBufferSize` before concatenating. +- On overflow: call `this.clear()` first (leave object in clean state), then throw `Error`. +- Fully backwards compatible — `new ReadBuffer()` with no args uses the default. + +### 2. StdioClientTransport (`packages/client/src/client/stdio.ts`) + +- Wrap the `stdout.on('data')` handler body in try/catch. +- On catch: route error to `this.onerror?.(error)`, then call `this.close()`. + +### 3. StdioServerTransport (`packages/server/src/server/stdio.ts`) + +- Wrap the `_ondata` handler body in try/catch. +- On catch: route error to `this.onerror?.(error)`, then call `this.close()`. + +### 4. Tests (`packages/core/test/shared/stdio.test.ts`) + +- `append()` throws when buffer exceeds default limit. +- `append()` throws with custom `maxBufferSize`. +- Buffer is cleared after overflow (object reusable). +- Default limit can be overridden via constructor. + +### 5. No changes to + +- Public API exports (`ReadBuffer` is already exported; constructor change is additive). +- `processReadBuffer()` in either transport (existing try/catch handles `readMessage()` errors; new try/catch handles `append()` errors at a higher level). + +## Files Modified + +| File | Change | +|------|--------| +| `packages/core/src/shared/stdio.ts` | Add `DEFAULT_MAX_BUFFER_SIZE`, constructor options, size guard in `append()` | +| `packages/client/src/client/stdio.ts` | try/catch in `data` handler, close on overflow | +| `packages/server/src/server/stdio.ts` | try/catch in `_ondata` handler, close on overflow | +| `packages/core/test/shared/stdio.test.ts` | New tests for buffer overflow behavior | + +## Decision Log + +- **10 MB default** chosen because a single JSON-RPC message shouldn't realistically exceed a few MB (even a 7 MB binary base64-encoded is ~9.3 MB). Users with legitimate large messages can raise the cap explicitly. +- **Throw from append()** rather than silent truncation or callback — uses existing error propagation paths and makes the failure visible. +- **Clear before throw** so the ReadBuffer isn't left in a corrupt state. +- **Close transport on overflow** because a buffer overflow means the peer is misbehaving and any partial data is unrecoverable. +- **No chunk-list optimization** — the 10 MB cap bounds the `Buffer.concat()` amplification to ~50 MB worst case, which is acceptable. Chunk-list can be a separate follow-up. +- **Options object** (not bare number) for the constructor parameter, for future extensibility. diff --git a/docs/superpowers/specs/2026-06-08-sep-2549-ttl-design.md b/docs/superpowers/specs/2026-06-08-sep-2549-ttl-design.md new file mode 100644 index 0000000000..f59499ed9c --- /dev/null +++ b/docs/superpowers/specs/2026-06-08-sep-2549-ttl-design.md @@ -0,0 +1,495 @@ +# SEP-2549: TTL for List Results — Design + +**Status:** Draft for review (rev 2 — incorporates backend + software architecture review) +**Date:** 2026-06-08 +**SEP:** [2549 — TTL for List Results](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/docs/seps/2549-TTL-for-list-results.mdx) +**Branch:** `feature/v2-SEP-2549-ttl-for-list-results` + +## Context + +MCP clients currently discover changes to a server's tools/prompts/resources only via `list_changed` notifications, which require a long-lived SSE stream. Many HTTP clients and servers cannot reliably hold such streams, and there is no signal for how often a list actually changes. SEP-2549 adds two freshness fields — `ttlMs` and `cacheScope` — to the five cacheable result types so a server can tell clients how long a response stays fresh and who may cache it. This works alongside notifications (not as a replacement) and is fully backward-compatible with pre-2549 servers. + +The upstream draft spec (`schema/draft/schema.ts`) already defines these types. This work brings the TypeScript SDK to parity and — critically — makes the SDK actually *use* the hints: a client SDK that only emits the wire fields satisfies nothing of value, because `client.listTools()` would still refetch every time and discard the TTL. The deliverable therefore spans the wire types, server emission, a client-side cache, polling helpers, and a shared (multi-tenant) cache. + +### Scope decision + +Full implementation, all acceptance criteria. Confirmed during brainstorming: +- **Layer 5 (shared multi-tenant cache, R-2549-7): in scope** — ship it now. +- **Client cache enablement: `ClientOptions` flag** — cache logic lives in `Client`, off by default for backward compatibility. + +### Delivery: two PRs + +This design is implemented and reviewed as **two independent PRs**, because the API surface and risk profiles are very different: + +- **PR 1 — wire + server emission (Layers 1–2).** Low-risk, independently valuable, satisfies R-2549-1/3/8/12/13. Adds the spec-parity types and McpServer emission config. Ships first. +- **PR 2 — client cache, polling, shared store (Layers 3–5).** Carries all the contested surface (read-through caching, invalidation, multi-tenant isolation, polling). Built on top of PR 1, reviewed on its own. + +Both PRs are described here as one design for coherence; the File Summary marks which PR each file belongs to. + +--- + +## The two safety invariants + +Two invariants are load-bearing for the entire feature and every layer below depends on them. They are stated once here and tested explicitly. + +> **Invariant A — `ttlMs: 0` is never cached.** +> An entry with `ttlMs === 0` is never stored, never returned as fresh, and never shared across principals. This is what makes the feature backward-compatible (pre-2549 servers normalize to `ttlMs: 0` ⇒ behave exactly like today) **and** what makes a `cacheScope: 'public'` *default* safe at the wire layer (a defaulted-public entry with `ttlMs: 0` can never be served from any cache, shared or not). + +> **Invariant B — only *explicitly-declared* `cacheScope: 'public'` may be shared.** +> A shared cache (Layer 5) shares an entry across principals **only if the server explicitly sent `cacheScope: 'public'` on the wire.** An absent `cacheScope` — even though it normalizes to `'public'` for wire-type parity — is treated by the cache as *unknown ⇒ private ⇒ never shared.* This resolves the SEP's internal contradiction (see below) in the fail-safe direction: a misclassification costs at most a cache miss, never a cross-tenant data leak. + +### The SEP contradiction these invariants resolve + +The SEP is internally contradictory about the default for an absent `cacheScope`: + +- The `CacheableResult` JSDoc says: *"Defaults to `"public"` if absent."* +- The Backward Compatibility section says the opposite, and explains why: *"`cacheScope` is required because there is no safe default for older servers. The server must explicitly declare the intended cache scope to prevent unintended caching of user-specific data."* It calls out `resources/read` as user-specific (private). + +We honor **both** by separating two concerns the first draft conflated: + +1. **Wire-type normalization (Layer 1):** absent `cacheScope` → `'public'` *as a type-level default only*, so the SDK output type has the required field the spec demands (parity). This default value is never, by itself, an authorization to share — Invariant A guarantees a defaulted entry (which also has `ttlMs: 0` unless the server set a TTL) cannot be cached. +2. **Caching authorization (Layers 3 & 5):** the cache tracks whether `cacheScope` was *explicitly present on the wire* (`scopeExplicit`) and shares cross-principal only when it was explicitly `'public'` (Invariant B). + +--- + +## Acceptance Criteria → Layer Map + +| ID | Requirement | Layer | PR | +|----|-------------|-------|----| +| R-2549-1 (wire) | Server MUST include `ttlMs` (≥0) and `cacheScope` on the 5 result types | 1 + 2 | 1 | +| R-2549-3 (guidance) | Per-page `ttlMs`; freshness lives on the *result*, not on `Tool`/`Resource` | 1 | 1 | +| R-2549-12 | Absent `ttlMs` → treat as 0 (BC with pre-2549 servers) | 1 | 1 | +| R-2549-13 | Negative `ttlMs` → treat as 0 | 1 | 1 | +| R-2549-8 | Same `cacheScope` on every page of a paginated response | 2 | 1 | +| R-2549-2 | Client SHOULD refetch on next access after `ttlMs` expires; MAY serve stale on refetch error | 3 | 2 | +| R-2549-11 | `list_changed` / `resources/updated` invalidates the cache regardless of remaining TTL | 3 | 2 | +| R-2549-14 | Cursor invalid (`-32602` on next-page fetch) → discard cached pages, refetch from page 1 | 3 | 2 | +| R-2549-10 (sdk) | Polling helpers MUST apply jitter + backoff | 4 | 2 | +| R-2549-7 (security) | Shared caches MUST NOT serve `private` entries to a different user (key on auth principal) | 5 | 2 | +| R-2549-4 | `list_changed` notifications still delivered if subscribed — TTL is a hint, not a replacement | (asserted by test) | 2 | + +## Architecture Overview + +``` +┌─ Layer 1: Wire types (core/types) ──────────────────────────────────┐ +│ CacheableResult { ttlMs: number; cacheScope: 'public'|'private' } │ +│ → spread into 5 result schemas; .default() normalization only │ +│ → normalizeCacheable() helper does clamp/floor + records scopeExplicit│ +└───────────────────────────────────────────────────────────────────────┘ + │ emitted by │ consumed by + ▼ ▼ +┌─ Layer 2: Server (server/mcp) ─┐ ┌─ Layer 3: Client cache (client) ──┐ +│ McpServerOptions.cache (hints) │ │ ListCacheStore (pluggable) │ +│ injects ttlMs/cacheScope into │ │ + InMemoryListCacheStore (default)│ +│ list & read results │ │ freshness, invalidation, cursor │ +└─────────────────────────────────┘ └────────────────────────────────────┘ + │ used by │ impl + ▼ ▼ + ┌─ Layer 4: pollList ─┐ ┌─ Layer 5: SharedListCacheStore ┐ + │ jitter + backoff │ │ shares only explicit-public; │ + │ (opt-in) │ │ private namespaced by principal│ + └──────────────────────┘ └────────────────────────────────┘ +``` + +--- + +## Layer 1 — Wire Types + +**Files:** `packages/core/src/types/schemas.ts`, `packages/core/src/types/types.ts`, `packages/core/src/types/spec.types.ts` (regenerated), `packages/core/test/spec.types.test.ts`. Public export of the two new **types** rides the one sanctioned `export *` from `types.ts` (see *Type exports* below — this is the only wildcard; all package-barrel symbols are explicit). + +### Spec parity is the hard constraint + +`packages/core/test/spec.types.test.ts` enforces, for every type, **bidirectional assignability** (`sdk = spec; spec = sdk`) and **exact key parity** (`AssertExactKeys`), operating on `z.output` (`Infer`). The upstream spec defines: + +```typescript +export interface CacheableResult extends Result { + ttlMs: number; // REQUIRED + cacheScope: "public" | "private"; // REQUIRED +} +export interface ListToolsResult extends PaginatedResult, CacheableResult { tools: Tool[]; } +// ...ListPromptsResult, ListResourcesResult, ListResourceTemplatesResult likewise +export interface ReadResourceResult extends CacheableResult { contents: (...)[]; } +``` + +Because the spec fields are **required**, an `.optional()` Zod field would fail mutual assignability (an `sdk` value with `ttlMs?: number` is not assignable to a spec value requiring `ttlMs: number`). Therefore the SDK output type must have these fields **required**, while still tolerating their absence on the wire (R-2549-12). + +> **Parity covers `z.output` only.** The parity test never exercises `z.input`, so the "tolerates absence on the wire" property (R-2549-12/13) is **not** guarded by parity — it is guarded solely by the schema unit tests below. The doc previously implied parity validated normalization; it does not. + +### Mechanism: `.default()` only (no `.transform()`) + +The first draft used `.default().transform(...)`. We drop the transform for three reasons surfaced in review: (a) a `ZodEffects`/transform field is brittle when spread into objects that may later be `.extend()`/`.pick()`/`.partial()`ed; (b) it makes the normative clamp (negative→0, R-2549-13) invisible inside a field spread; (c) it widens the `z.input`/`z.output` skew more than necessary and runs again on any (future) outbound validation. Clamping moves into one named helper. + +```typescript +export const CacheScopeSchema = z.enum(['public', 'private']); + +// Field spread for the 5 result schemas. Plain .default() — output type is required, +// input is optional. No transform. +const cacheableResultFields = { + ttlMs: z.number().default(0), + cacheScope: CacheScopeSchema.default('public'), +}; +``` + +- `Infer` (z.output) = `{ ttlMs: number; cacheScope: 'public' | 'private' }` → **matches spec exactly** (parity passes). +- z.input allows both omitted → defaults applied at the parse boundary. The SDK validates results on the **receiving (client) side**, so absent `ttlMs` becomes `0` and absent `cacheScope` becomes `'public'` automatically. + +> **Spike the exact Zod v4 form before building Layer 3.** Confirm that `z.number().default(0)` spread into a `looseObject` result schema yields `z.output` with a *required* `number` and passes `AssertExactKeys` against the regenerated spec type. This is a 30-minute compile-only spike; do it first (it was the original Open Risk #1). + +### Normalization + the `scopeExplicit` signal + +Clamp/floor and the explicitness signal live in one helper consumed by the client cache (Layer 3). It runs on the **already-parsed** result *and* inspects the **raw** (pre-parse) payload to learn whether `cacheScope` was actually present on the wire: + +```typescript +// packages/core/src/types/... (exported for client use) +export interface NormalizedCacheMeta { + ttlMs: number; // clamped: negative/NaN/Infinity → 0, floored to int + cacheScope: CacheScope; // parsed value (defaulted to 'public' if absent) + scopeExplicit: boolean; // TRUE only if the raw wire payload contained `cacheScope` +} + +export function normalizeCacheMeta(parsed: CacheableResult, raw: unknown): NormalizedCacheMeta { + const ttlMs = Number.isFinite(parsed.ttlMs) && parsed.ttlMs > 0 ? Math.floor(parsed.ttlMs) : 0; // R-2549-12/13 + const scopeExplicit = typeof raw === 'object' && raw !== null && 'cacheScope' in raw; + return { ttlMs, cacheScope: parsed.cacheScope, scopeExplicit }; +} +``` + +This is the **single source of truth** for normalization. `scopeExplicit` is the signal Invariant B needs and that a bare `.default('public')` would otherwise erase. The client cache stores it on `CachedEntry`; `SharedListCacheStore` reads it to decide shareability. + +Spread `...cacheableResultFields` into the 5 result schemas only: `ListToolsResultSchema`, `ListPromptsResultSchema`, `ListResourcesResultSchema`, `ListResourceTemplatesResultSchema`, `ReadResourceResultSchema`. **Never** added to item schemas (`Tool`, `Resource`, etc.) — freshness lives on the result (R-2549-3). `ListRootsResult`, `PaginatedResult`, and `ResultSchema` are untouched. + +### Type exports & spec test + +- Add `CacheScope` and `CacheableResult` **type** exports in `types.ts`. These become public via the *one intentional* `export * from './types/types.js'` in `core/public` (documented there as the sanctioned wildcard). **All other new symbols** (`ListCacheStore`, `InMemoryListCacheStore`, `SharedListCacheStore`, `CacheHints`, `McpServerOptions`, `pollList`, etc.) live in the client/server packages and MUST be added as **explicit named exports** in `packages/client/src/index.ts` / `packages/server/src/index.ts` — never via `export *`. (The earlier draft's "transitively via `export *`" framing applied *only* to the two core types; do not let it leak into the package barrels.) +- The spec defines `CacheableResult` as a base interface with no standalone schema; the SDK mirrors it as an exported **type**, while the runtime schema is the `cacheableResultFields` spread. The exported type and the spread fields therefore have **no shared schema** and must be hand-kept in sync — call this out as a known manual-sync seam (it mirrors how `PaginatedResult` is handled today). +- `CacheableResult` is marked `@internal` in the upstream spec. We deliberately export it as public SDK API anyway, because it is the natural return-type contract for low-level handler authors (see ADR-001). Note the divergence in the export comment. +- Run `pnpm fetch:spec-types` first — the committed `spec.types.ts` is stale (commit `5c25208…`) and predates these fields. (`spec.types.ts` is `.gitignore`d and regenerated by `pnpm test`.) +- Add a `CacheableResult` entry to `sdkTypeChecks` and a `_K_CacheableResult` key-parity assertion in `spec.types.test.ts`. Update the expected spec-type count (`toHaveLength(176)` → new count). **Verify the `_meta`/index-signature interplay:** `ResultSchema` is a `looseObject` (carries an index signature), so confirm `AssertExactKeys` for `CacheableResult` resolves to exactly `{ ttlMs, cacheScope, _meta? }` and that the loose index signature neither makes the assertion trivially pass nor spuriously fail. + +### Breaking change — see ADR-001 + +Low-level `Server.setRequestHandler('tools/list', …)` handlers (and the other four) now have a return type requiring `ttlMs`/`cacheScope`. This is a **deliberate, recorded** breaking change, not an accident of typing. Rationale, the rejected alternative, and the blast radius are in **ADR-001** below. McpServer injects the fields so high-level authors are unaffected (Layer 2). Documented in `docs/migration.md` + `docs/migration-SKILL.md`. + +--- + +## ADR-001 — Breaking change to low-level `Server` handler return types + +**Status:** Accepted · **Context:** Layer 1/2 · **Decision owners:** SEP-2549 implementers + +**Context.** With `.default()`, the result schemas' `z.output` (= `Infer` = the public `ListToolsResult` type, and the type `ResultTypeMap['tools/list']` against which `setRequestHandler` types a handler's return value) has `ttlMs`/`cacheScope` as **required**. So every low-level handler for the 5 methods must now return both fields or fail to type-check. McpServer's own internal handlers are also consumers of this type and are updated in Layer 2. + +**The alternative considered.** Type handler returns against `z.input` (where `.default()` makes the fields optional) while keeping the public wire type as `z.output`. That would make the change *non-breaking* for low-level authors. + +**Why we reject it.** The protocol does **not** re-validate or re-parse outbound results through the result schema — outbound results are serialized as-is; only the *receiving* side parses (verified in `protocol.ts` request/response path). So `z.input`-typed handlers would put **no** `ttlMs`/`cacheScope` on the wire, silently violating R-2549-1 ("server MUST include"). Making the change non-breaking would require introducing (a) input/output result-type *duality* across `ResultTypeMap`/`InferHandlerResult` and (b) a new outbound-normalization pass that does not exist today — a larger, more invasive change than the break, and one that trades a compile-time error for a *silent* spec violation. + +**Decision.** Keep the breaking change. Surfacing the "server MUST include" obligation as a compile-time error on low-level handlers is the spec-faithful, fail-loud choice. + +**Consequences.** (1) Low-level `Server` users for the 5 methods must add the two fields — migration guide ships the exact two-field snippet and points to `normalizeCacheMeta`/`CacheableResult`. (2) McpServer is itself a consumer and is updated in the same PR. (3) If a future SEP needs non-breaking result-type evolution, the input/output duality is the path — recorded here so it isn't re-litigated. + +--- + +## Layer 2 — Server Emission + +**Files:** `packages/server/src/server/mcp.ts`, `packages/server/src/index.ts`. **(PR 1)** + +```typescript +// Renamed from the draft's `ListCacheConfig`: this is emission config (freshness hints), +// not a cache. Avoids prefix-collision with the client's ListCache* types. +export interface CacheHints { ttlMs?: number; cacheScope?: 'public' | 'private'; } + +export interface McpServerOptions extends ServerOptions { + cache?: { + tools?: CacheHints; + resources?: CacheHints; + resourceTemplates?: CacheHints; + prompts?: CacheHints; + resourceRead?: CacheHints; // hints for resources/read + }; +} +``` + +- `McpServer` constructor widens `options?: ServerOptions` → `McpServerOptions` (backward-compatible) and stores `_cacheOptions`. +- The 4 list handlers spread `{ ttlMs: cfg?.ttlMs ?? 0, cacheScope: cfg?.cacheScope ?? 'public' }` into their results. Because config is static per endpoint, **every page gets the same `cacheScope`** (R-2549-8) for free. +- `resources/read`: callback result is normalized — `ttlMs`/`cacheScope` from the callback win; otherwise fall back to `cache.resourceRead` config, then defaults. The `ReadResourceCallback` return type is loosened so callbacks may omit the fields; McpServer fills them. +- **`resources/read` privacy footgun (documented).** The SEP calls out `resources/read` as the user-specific endpoint. The emission default is `cacheScope: 'public'` only because the paired default `ttlMs: 0` makes it uncacheable (Invariant A). **The migration guide MUST warn:** if you configure a non-zero `ttlMs` on `resourceRead` (or return one from the callback) for user-specific content, you MUST also set `cacheScope: 'private'`, or a downstream shared gateway may cache it. As defense-in-depth, when `cache.resourceRead.ttlMs > 0` is configured but `cacheScope` is omitted, McpServer emits `'private'` (not `'public'`) for `resources/read` specifically. +- **R-2549-8 for low-level servers.** McpServer guarantees same-scope-per-page structurally (static config). A low-level `Server` author paginating by hand could still vary `cacheScope` across pages; this is the author's responsibility per the spec MUST. We document it; we do not add a runtime guard (the SDK does not own the low-level pagination loop). +- Export `CacheHints`, `McpServerOptions` from `@modelcontextprotocol/server` as **explicit named exports**. + +--- + +## Layer 3 — Client-Side List Cache + +**Files:** new `packages/client/src/client/listCache.ts`; modify `packages/client/src/client/client.ts`, `packages/client/src/index.ts`. **(PR 2)** + +### Pluggable store + +```typescript +export interface ListCacheKey { + method: 'tools/list' | 'prompts/list' | 'resources/list' | 'resources/templates/list' | 'resources/read'; + cursor?: string; // pagination position + uri?: string; // for resources/read + principal?: string; // supplied by caller for shared caches (Layer 5); undefined for per-client +} + +export interface CachedEntry { + result: unknown; + receivedAt: number; + ttlMs: number; + cacheScope: CacheScope; + scopeExplicit: boolean; // from normalizeCacheMeta — gates cross-principal sharing (Invariant B) +} + +export interface ListCacheStore { + get(key: ListCacheKey): CachedEntry | undefined; + set(key: ListCacheKey, entry: CachedEntry): void; + /** Invalidate entries for a method. See the invalidation matrix below for principal/uri semantics. */ + invalidate(method: ListCacheKey['method'], opts?: { uri?: string; principal?: string }): void; + clear(): void; +} + +export class InMemoryListCacheStore implements ListCacheStore { /* Map-backed, single-tenant */ } +``` + +### Client-local request options (no core pollution) + +`principal`/`bypassCache` are **client-cache concerns and do not belong in core `RequestOptions`** (which is the transport-agnostic, server-and-client per-call bag). They are added in a **client-local** extension instead, keeping the core protocol type pure: + +```typescript +// packages/client/src/client/client.ts +export type ListRequestOptions = RequestOptions & { + principal?: string; // routing key for a shared cache (Layer 5); MUST be the validated auth principal + bypassCache?: boolean; // force refetch, then refresh the cache entry +}; +``` + +The list/read methods widen their `options?: RequestOptions` parameter to `ListRequestOptions` locally. Core `shared/protocol.ts` is **not** modified. + +### ClientOptions + +```typescript +export type ClientOptions = ProtocolOptions & { + // ...existing... + cache?: { + store?: ListCacheStore; // default: new InMemoryListCacheStore() when `cache` present + serveStaleOnError?: boolean; // R-2549-2 "MAY"; default true (resilience) + now?: () => number; // injectable clock for tests; default Date.now (runtime-neutral) + }; +}; +``` + +Absent `cache` ⇒ no caching, current behavior preserved (BC). + +### Read-through in list/read methods + +`listTools`, `listResources`, `listResourceTemplates`, `listPrompts`, `readResource` gain a cache path when caching is enabled: + +1. Build `ListCacheKey` (method + cursor/uri + optional `principal` from `ListRequestOptions`). If `bypassCache` → skip step 2. +2. `entry = store.get(key)`. **Fresh** if `now() < entry.receivedAt + entry.ttlMs` → return cached result. +3. On miss/stale: perform the request. Run `normalizeCacheMeta(parsed, raw)`. **Invariant A:** if `ttlMs === 0`, return the result but **do not** `store.set` (never cache a zero-TTL entry). Otherwise `store.set` with `receivedAt = now()` and the normalized `ttlMs`/`cacheScope`/`scopeExplicit`. +4. On **refetch error** with a prior entry present and `serveStaleOnError` (default true): return the stale result (R-2549-2). Otherwise propagate. + +### Notification invalidation (R-2549-11) + +> **Fix from review:** the existing `_setupListChangedHandlers` only registers when the user passed a `listChanged` config **and** the server advertised the capability — the common cache-user case (no `listChanged` config) would get **no** invalidation, serving stale until TTL expiry and violating R-2549-11. + +When **caching is enabled**, the client registers cache-invalidation notification handlers **unconditionally** — independent of the `listChanged` config and independent of the server capability gate. Because `setNotificationHandler` replaces by method (and would clobber a user's `listChanged` handler), invalidation is wired through an **internal dispatcher**: a single registered handler per notification method that first runs cache invalidation, then delegates to any user-supplied handler. The existing `_setupListChangedHandlers` is refactored to register *through* this dispatcher rather than directly, so cache invalidation and user `onListChanged` callbacks **compose** instead of overwriting each other. + +- `notifications/tools/list_changed` → `store.invalidate('tools/list')` +- `notifications/prompts/list_changed` → `store.invalidate('prompts/list')` +- `notifications/resources/list_changed` → `store.invalidate('resources/list')` **and** `store.invalidate('resources/templates/list')` (conservative; over-invalidates templates on a resource-list change — acceptable, documented) +- `notifications/resources/updated` → `store.invalidate('resources/read', { uri })` for the updated URI + +Invalidation is immediate, regardless of remaining TTL. + +> **`resources/read` invalidation is subscription-dependent (documented).** `notifications/resources/updated` is only sent by the server if the client previously sent `resources/subscribe` for that URI. So notification-driven read-cache invalidation is satisfied **for subscribed URIs only**; for unsubscribed reads, the TTL is the sole staleness bound. Enabling the read cache does **not** auto-subscribe (left to the caller, by design). R-2549-11 status: *satisfied for subscribed resources; TTL-bounded otherwise.* + +### Cursor invalidation (R-2549-14) + +> **Fix from review:** the draft treated *any* `-32602` on *any* cursored fetch as "drop all pages." `-32602` (InvalidParams) is generic and can mean a malformed `params` unrelated to the cursor, masking real bugs in a surprising full-refetch loop. + +Narrowed trigger: the special handling fires **only** when (a) the failing request carried a `cursor` that this cache itself issued from a prior page of the **same list traversal** (a *cache-originated* cursor), **and** (b) the server replied with `ProtocolError` code `-32602`. On both: + +1. `store.invalidate(method)` — drop all cached pages for that list. +2. Re-fetch from page 1 (no cursor). This retry runs with cursor-invalidation **structurally disabled** (it cannot recurse into another page-drop — enforced by a flag scoped to the retry, not by convention), so a second failure propagates. +3. `log`/emit a debug signal when pages are collapsed, so a genuine `-32602` bug is not silently swallowed. + +> **No snapshot isolation across a traversal (documented).** The cache provides **per-page freshness**, not a consistent snapshot of a full paginated list. Each page has its own freshness clock (R-2549-3); a mid-traversal refetch (TTL expiry or cursor invalidation) can yield a page 1 different from the one the caller already consumed. Callers needing a consistent full-list snapshot SHOULD iterate with `bypassCache` or refetch from the beginning, per the SEP. + +--- + +## Layer 4 — Polling Helpers + +**Files:** new `packages/client/src/client/pollList.ts`; export (explicit named) from `packages/client/src/index.ts`. **(PR 2)** + +```typescript +export interface PollListOptions { + onUpdate: (result: unknown) => void; + onError?: (error: unknown) => void; + signal?: AbortSignal; + minIntervalMs?: number; // floor on the poll interval; default 30_000 (see below) + jitter?: number; // fraction, default 0.2 (±20%) + backoff?: { initialMs?: number; maxMs?: number; factor?: number }; // on error + backoffOnUnchanged?: boolean; // grow interval when results are unchanged; default true +} + +// ttlMs is read internally off the response — callers don't extract it. +export function pollList( + client: Client, + method: 'tools/list' | 'prompts/list' | 'resources/list' | 'resources/templates/list', + options: PollListOptions, +): { stop: () => void }; +``` + +> **Fix from review:** the draft's `fetch: () => Promise<{ result, ttlMs }>` leaked `ttlMs` extraction to the caller. `pollList` now takes the `client` + `method` and reads `ttlMs` off the normalized response itself. + +- Base interval seeded from the last response's `ttlMs`, **clamped up** to `minIntervalMs`. +- **`minIntervalMs` default is `30_000`, not `1_000`.** A `ttlMs: 0` server (very common — every pre-2549 server) would otherwise drive a 1-req/sec hammer per list. Polling a `ttlMs: 0` server is **degenerate**; the docs warn against it and the high floor blunts the damage. +- **Jitter** (MUST per R-2549-10): each delay multiplied by `1 ± jitter*random` to avoid thundering herd. +- **Backoff** (MUST per R-2549-10): on error, exponential backoff (`initialMs * factor^n`, capped at `maxMs`) until next success. +- **Backoff on unchanged** (default on): when a poll returns a result equal to the previous one, grow the interval (same capped exponential) so a chatty poller relaxes against a static list; reset on change. Without this, a poller never backs off a never-changing list. +- Opt-in only. Reconciles the SEP's "clients SHOULD NOT use TTL as a polling interval" guidance with R-2549-10: the *default* path is freshness-on-access (Layer 3); `pollList` is a separate utility for callers who explicitly want background refresh, and when used it MUST jitter+backoff. **Docs steer hard toward `pollList`** and explicitly warn against naive `setInterval(ttlMs)` polling — the SDK cannot enforce that a caller won't hand-roll a poll loop, so guidance carries the MUST's intent. + +> **MUST vs SHOULD note (in-code comment).** The SEP MD says polling *SHOULD* jitter+backoff; the canonical `caching.mdx` spec page (and acceptance criterion R-2549-10) escalate this to *MUST*. We implement the stricter MUST. A code comment records the source of the stricter rule so it isn't "relaxed" later by someone reading only the SEP MD. + +--- + +## Layer 5 — Shared (Multi-Tenant) Cache + +**Files:** `packages/client/src/client/listCache.ts` (add `SharedListCacheStore`); export (explicit named) from `packages/client/src/index.ts`. **(PR 2)** + +A `ListCacheStore` implementation for gateways/proxies that front multiple end users through one upstream `Client`. + +### Sharing rule (Invariant B) + +- An entry is shared across principals **only if** `entry.cacheScope === 'public'` **and** `entry.scopeExplicit === true`. A defaulted-public entry (`scopeExplicit === false`) is treated as **private** and is never served to a different principal — fail-safe against pre-2549 / misconfigured servers. +- `cacheScope: 'private'` (or non-explicit) entries are namespaced by `key.principal`. A `get` for such an entry with a non-matching (or absent) principal returns `undefined`. +- Combined with Invariant A (`ttlMs: 0` never stored), this closes the leak path the first draft had: an absent-`cacheScope` user-specific response can never be served cross-user. + +### Principal contract (security-critical) + +- The **principal is supplied by the caller per request** via `ListRequestOptions.principal`. The SDK does not infer identity. +- The principal **MUST be derived from the validated auth principal** (e.g. `ctx.http.authInfo`), an **opaque, stable identifier** — never from request-supplied, attacker-influenceable headers. This is stated as an enforced contract in the API docs, not a passing comment. +- Principal strings are compared **as-is** (no normalization): the store treats distinct representations as distinct namespaces. This is the safe direction (over-isolation, never under-isolation); callers MUST pass a canonical ID. Documented. +- A `private` (or non-explicit) `set` **without** a principal is a **no-op** (the entry cannot be safely isolated, so it is not stored) — documented and tested. + +### Invalidation matrix + +`invalidate(method, opts)` semantics, made explicit (the draft's interface couldn't express these): + +| Event | `opts` | Effect | +|-------|--------|--------| +| `list_changed` for a shared/public list | `{}` | Invalidate the shared public entry for **all** principals **and** every principal's private copy of that method | +| `resources/updated` for a private resource | `{ uri, principal }` | Invalidate only that **principal's** entry for that **uri** | +| `resources/updated`, principal unknown | `{ uri }` | Invalidate that `uri` across **all** principals (conservative) | +| explicit per-principal clear | `{ principal }` | Invalidate that principal's entries for the method | + +Isolation is the security-critical property and gets dedicated tests (below), including the **absent-`cacheScope`** case — not just the explicitly-`private` case — because that is the actual leak path. + +--- + +## R-2549-4 — Notifications Coexist + +No new machinery: `list_changed` notifications already flow through the notification path independent of TTL, and Layer 3's dispatcher composes cache invalidation **with** user `onListChanged` handlers. A test asserts that with caching enabled, a server advertising `listChanged` still delivers notifications **and** the client honors TTL — the two mechanisms layer (notification invalidates immediately; TTL bounds staleness otherwise). + +--- + +## Runtime neutrality + +`packages/client/src/index.ts` is the package root entry and MUST stay runtime-neutral (browser / Cloudflare Workers — no transitive `node:*`). The new modules `listCache.ts` and `pollList.ts` are re-exported from it, so: + +- Both modules MUST be pure JS — no `node:*`, no `crypto` import for principal hashing (principals are opaque caller-supplied strings; the store does not hash). `now` defaults to `Date.now` (neutral) and is injectable. +- Add `listCache.ts` and `pollList.ts` to the package's **`barrelClean` test** so a future accidental Node import is caught. + +--- + +## Testing Strategy + +Tests are TDD-first, one behavior at a time. Layered by package. + +**Core (`packages/core`) — PR 1:** +- `spec.types.test.ts` parity for `CacheableResult` + the 5 result types (compile-time), incl. the `_meta`/looseObject key-parity check. +- Schema unit tests (the **sole** guard for the input contract): absent `ttlMs` → 0; negative → 0; NaN/Infinity → 0; non-integer floored; absent `cacheScope` → `'public'`; explicit values preserved. +- `normalizeCacheMeta`: `scopeExplicit` true when raw payload has `cacheScope`, false when absent; clamp behavior. + +**Server (integration, `test/integration/test/server/mcp.test.ts`) — PR 1:** +- Each of the 4 list endpoints + `resources/read` emits configured `ttlMs`/`cacheScope`. +- `resources/read` callback values override config. +- `resources/read` with `ttlMs > 0` configured + `cacheScope` omitted → emits `'private'` (defense-in-depth). +- Same `cacheScope` across paginated pages (R-2549-8). +- Backward compat: no config → fields present with defaults (0/'public'). +- Low-level `Server` handler can (and must) return the fields directly (ADR-001). + +**Client (`test/integration/test/client/` + unit) — PR 2:** +- Fresh hit served from cache without a wire request; stale triggers refetch (R-2549-2), using injectable `now()`. +- **Invariant A:** `ttlMs: 0` result is never stored and always refetches (R-2549-12). +- Refetch error serves stale when `serveStaleOnError` (default), propagates when off. +- Notification invalidation for each type (R-2549-11), regardless of remaining TTL — **including the no-`listChanged`-config case** (handlers register unconditionally) and the **dispatcher composition** test (cache invalidation + user `onListChanged` both fire, neither clobbers the other). +- `resources/updated` invalidation works for a subscribed URI; documented gap asserted for unsubscribed. +- Cursor `-32602` on a **cache-originated** cursor → drop pages, refetch from page 1; retry is non-recursive (second failure propagates); a `-32602` on a non-cache-originated/non-cursored request does **not** trigger page-drop. +- `pollList`: jitter bounds the interval; backoff grows on consecutive errors then resets; backoff-on-unchanged grows then resets on change; `minIntervalMs` floor honored with `ttlMs: 0` (R-2549-10) — deterministic via injected clock + seeded randomness. +- **`SharedListCacheStore` (R-2549-7):** explicitly-private entry not served cross-principal; **absent-`cacheScope` entry not served cross-principal** (the real leak path); explicit-public entry shared; private `set` without principal is a no-op; the full invalidation matrix. +- Caching disabled by default → behavior identical to today (BC). +- `barrelClean` covers `listCache.ts` / `pollList.ts` (runtime neutrality). + +**E2E requirements manifest (`test/e2e/requirements.ts`):** +Register `caching:*` requirement entries linked to scenario tests so the conformance suite tracks coverage, mirroring existing entries: `caching:ttl:emitted`, `caching:client:freshness`, `caching:invalidate:list-changed`, `caching:invalidate:list-changed:no-config`, `caching:invalidate:resource-updated`, `caching:cursor:invalid`, `caching:scope:isolation`, `caching:scope:isolation:absent-scope`, `caching:poll:jitter-backoff`. Add scenario tests under `test/e2e/scenarios/`. + +## Documentation + +- `docs/migration.md` — human-readable: new fields, McpServer cache (`CacheHints`) config, the **`resources/read` privacy warning**, low-level `Server` breaking change (ADR-001) with the exact two-field snippet, client cache opt-in, the shared-cache **principal contract**, `pollList` (and the anti-pattern warning against `setInterval(ttlMs)`). +- `docs/migration-SKILL.md` — symbol mapping table (`CacheableResult`, `CacheScope`, `CacheHints`, `McpServerOptions`, `ListCacheStore`, `InMemoryListCacheStore`, `SharedListCacheStore`, `ListRequestOptions`, `pollList`, `normalizeCacheMeta`, schema extensions). +- A changeset under `.changeset/` per PR. + +## File Summary + +| Action | File | Layer | PR | +|--------|------|-------|----| +| Modify | `packages/core/src/types/schemas.ts` | 1 | 1 | +| Modify | `packages/core/src/types/types.ts` (+ `normalizeCacheMeta`, `CacheScope`, `CacheableResult`) | 1 | 1 | +| Regenerate | `packages/core/src/types/spec.types.ts` | 1 | 1 | +| Modify | `packages/core/test/spec.types.test.ts` | 1 | 1 | +| Modify | `packages/server/src/server/mcp.ts` | 2 | 1 | +| Modify | `packages/server/src/index.ts` (explicit exports: `CacheHints`, `McpServerOptions`) | 2 | 1 | +| Create | `packages/client/src/client/listCache.ts` | 3, 5 | 2 | +| Modify | `packages/client/src/client/client.ts` (`ListRequestOptions`, read-through, dispatcher) | 3 | 2 | +| Create | `packages/client/src/client/pollList.ts` | 4 | 2 | +| Modify | `packages/client/src/index.ts` (explicit named exports) | 3, 4, 5 | 2 | +| Modify | `test/integration/test/server/mcp.test.ts` | 2 | 1 | +| Create/Modify | `test/integration/test/client/*` | 3, 4, 5 | 2 | +| Modify | `test/e2e/requirements.ts` + `test/e2e/scenarios/*` | all | 1 & 2 | +| Modify | `docs/migration.md`, `docs/migration-SKILL.md`, `.changeset/*` | all | 1 & 2 | + +> **Note:** `packages/core/src/shared/protocol.ts` is **no longer modified.** `principal`/`bypassCache` moved to the client-local `ListRequestOptions` (Layer 3) to keep core protocol pure. + +## Review Findings → Resolution + +Traceability for the backend + software architecture review (rev 1 → rev 2): + +| Finding | Resolution | +|---------|------------| +| `cacheScope` default `'public'` leaks cross-tenant (CRITICAL ×2) | Invariants A & B; `scopeExplicit` on `CachedEntry`; `SharedListCacheStore` shares only explicit-public; `resources/read` defense-in-depth `'private'` | +| Notification invalidation gated by `listChanged` config/capability (CRITICAL) | Unconditional registration when caching on; internal dispatcher composes with user handlers; `no-config` test | +| `resources/updated` only fires when subscribed | Documented as subscription-dependent; R-2549-11 status qualified; asserted by test | +| Low-level `Server` breaking change rationale | ADR-001 (keep break; reject `z.input` softening; protocol does not back-fill outbound; McpServer is internal consumer) | +| `principal`/`bypassCache` pollute core `RequestOptions` | Moved to client-local `ListRequestOptions`; core untouched | +| `.transform()` brittle / lossy | Dropped; `.default()` + `normalizeCacheMeta` helper; `scopeExplicit` preserved | +| `-32602` cursor narrowing too broad | Scoped to cache-originated cursors; non-recursive single retry; logs on collapse; no-snapshot-isolation documented | +| Shared-cache bypass paths (F2) | Principal-derivation contract (auth principal only, opaque, no normalization); private-set-without-principal no-op; invalidation matrix; absent-scope test | +| Export plan `export *` framing | Split: two core types via sanctioned `export *`; all package-barrel symbols explicit named | +| `CacheableResult` `@internal` in spec | Deliberately public; divergence noted in export comment | +| Parity covers `z.output` only | Stated; input contract guarded by unit tests | +| `_meta`/looseObject key-parity interplay | Explicit verification step in spec-test bookkeeping | +| Type vs spread manual-sync seam | Called out (mirrors `PaginatedResult`) | +| `ListCacheConfig` prefix collision | Renamed server-side to `CacheHints` | +| `pollList` leaks `ttlMs` extraction | Signature takes `client` + `method`; reads `ttlMs` internally | +| `pollList` `minIntervalMs` 1s hammer / no unchanged-backoff | Default `30_000`; degenerate `ttlMs:0` warned; `backoffOnUnchanged` default on | +| Runtime neutrality of new modules | Pure-JS requirement + `barrelClean` coverage | +| API surface too large | Split into PR 1 (wire+server) and PR 2 (client cache) | +| `ttlMs:0` invariant under-stated | Promoted to Invariant A with dedicated test | +| R-2549-8 for low-level servers | Documented as author responsibility (no runtime guard) | + +## Open Risks + +1. **Exact Zod v4 `.default()` output-type form.** `z.number().default(0)` spread into a `looseObject` result schema must yield a *required* `number` in `z.output` and pass `AssertExactKeys`. De-risked by a compile-only spike done before Layer 3 (see Layer 1). Fallback: clamp/normalize entirely outside the schema (already the plan via `normalizeCacheMeta`). +2. **Dispatcher refactor of `_setupListChangedHandlers`.** Routing cache invalidation and user `onListChanged` through one composing dispatcher touches existing notification wiring. Covered by the composition test; verify no regression for users who configured `listChanged` without caching. +3. **Manual sync between exported `CacheableResult` type and the `cacheableResultFields` spread** (no shared schema). Low risk, mirrors `PaginatedResult`; parity test catches divergence at the result-type level. diff --git a/docs/superpowers/specs/2026-06-23-sdk-shared-package-design.md b/docs/superpowers/specs/2026-06-23-sdk-shared-package-design.md new file mode 100644 index 0000000000..4f9f4157c0 --- /dev/null +++ b/docs/superpowers/specs/2026-06-23-sdk-shared-package-design.md @@ -0,0 +1,135 @@ +# Design: `@modelcontextprotocol/sdk-shared` — canonical Zod schemas package + +- **Date:** 2026-06-23 +- **Status:** Approved (design); implementation plan pending +- **Owner:** Konstantin Konstantinov + +## Problem + +The v1→v2 migration of runtime schema validation is non-mechanical and lossy. + +In v1, consumers validated values with the exported Zod schema constants: + +```ts +import { CallToolResultSchema, ListToolsResultSchema } from '@modelcontextprotocol/sdk/types.js'; + +const parsed = ListToolsResultSchema.parse(res.body.result); // throws on invalid, returns value +const r = CallToolResultSchema.safeParse(value); // { success, data, error } +``` + +In v2 these schemas are reached via `specTypeSchemas.X` and **typed** as `StandardSchemaV1` (to keep Zod out of the public API), even though **at runtime they are still the underlying Zod schemas**. Because the public type is Standard Schema, `.parse()`/`.safeParse()` are not visible to the type checker, so the current codemod: + +- rewrites `CallToolResultSchema` → `specTypeSchemas.CallToolResult`, +- converts `.safeParse(x)` → `specTypeSchemas.X['~standard'].validate(x)` and remaps `.success`/`.data`/`.error` (which also changes the thrown error type), and +- has **no** one-line equivalent for `.parse()` (it throws; `validate()` does not), so those sites get a manual-migration diagnostic and don't compile until hand-edited. + +Validated against `firebase/firebase-tools`, this produced 4 post-codemod typecheck errors (all `.parse()`), plus project-type-resolution warnings on type-only files. + +A prior attempt (PR #2277) surfaces `parse()`/`safeParse()` on each `specTypeSchemas.X` entry as **type-only** methods and migrates by reference rename. That works but (a) pollutes the deliberately library-agnostic Standard Schema type with Zod-specific methods, and (b) only covers `parse`/`safeParse`, not other Zod methods (`.extend()`, `.merge()`, `.shape`, …). + +## Goals + +- Make schema-validation migration a **mechanical, behavior-preserving import-path swap**: `.parse()`/`.safeParse()` and every other Zod method keep working unchanged. +- Keep the `server`/`client` main API surface **Zod-free**; Zod coupling is opt-in and explicit. +- Establish a **canonical home for shared spec primitives** (schemas + types now, room for more later). +- Keep `specTypeSchemas`/`isSpecType` (the Standard Schema, library-agnostic view) intact and recommended for library-agnostic validation. + +## Non-goals + +- Changing the Standard Schema typing of `specTypeSchemas` (we are **not** adding `parse`/`safeParse` to it — this supersedes PR #2277's approach). +- Moving `Protocol`, transports, or validators. They stay in `core` and follow existing migration rules. +- Moving `specTypeSchemas`/`isSpecType` out of `core/public` (possible later; out of scope now). + +> **Update during implementation (Option C, user-approved):** the protocol **enums** (`enums.ts` → +> `ProtocolErrorCode`), **error classes** (`errors.ts` → `ProtocolError`, …), and **type guards** +> (`guards.ts`) were *also* moved into `sdk-shared`, reversing the original "they stay in core" +> non-goal. Rationale: v1's `sdk/types.js` was a kitchen-sink exporting all of these alongside the +> spec types/schemas, so the codemod's `types.js → sdk-shared` routing is only correct if sdk-shared +> carries that whole surface. Their dependency closure (schemas/types/enums) is already in sdk-shared, +> so the move is clean and introduces no cycle. **Exception:** `SdkError`/`SdkErrorCode`/`SdkHttpError` +> (the SDK-side error split, in `core/errors/sdkErrors.ts`) deliberately stay in `core` → `server`/`client`. + +## Decisions (locked) + +| Decision | Choice | +| --- | --- | +| Package name | `@modelcontextprotocol/sdk-shared` | +| Scope of move | Spec **types + Zod `*Schema` constants** | +| Positioning | Zod schemas are **first-class** (no codemod nudge toward `specTypeSchemas`) | +| server/client bundling | Depend on `sdk-shared` as a **regular dependency**, marked **external** (not bundled) | +| server/client re-exports | Re-export **types** from `sdk-shared`; do **not** re-export the raw Zod `*Schema` constants | +| Consumer dependency | Regular `dependency` (not peer); codemod adds it | +| core churn control | `core`'s internal barrel **re-exports** schemas/types from `sdk-shared` | + +## Architecture + +### Package + +New public package `packages/sdk-shared/` (`@modelcontextprotocol/sdk-shared`): + +- Owns the canonical MCP spec data model: the Zod `*Schema` constants and their derived TS types (`Tool`, `CallToolResult`, …), extracted from `packages/core/src/types/types.ts`. +- Depends only on `zod` (catalog: `runtimeShared`). **Runtime-neutral** — no Node builtins — so browser/Cloudflare Workers bundlers can consume it (covered by a `barrelClean` test, per CLAUDE.md). +- Uses explicit named exports. + +### Dependency graph (new edges in **bold**) + +``` +zod + └── @modelcontextprotocol/sdk-shared (NEW — types + Zod *Schema constants; zod-only) + ├── @modelcontextprotocol/core (private; imports schemas/types from sdk-shared, re-exports them from its barrel) + ├── **@modelcontextprotocol/server** ─┐ regular dependency, + └── **@modelcontextprotocol/client** ─┘ marked EXTERNAL in tsdown (not bundled) +``` + +`server`/`client` today inline `core` (and thus the schemas). After this change they treat `@modelcontextprotocol/sdk-shared` as an external dependency, so there is a single runtime instance and their bundles shrink. (Instance identity is not a correctness concern — validation is structural — so "single instance" is about source-of-truth and bundle size, not behavior.) + +### What moves vs. stays + +- **Moves to `sdk-shared`:** the spec Zod `*Schema` constants and their inferred TS types. `types.ts` is **split** along this line; the exact boundary (pure spec schemas + inferred types move; protocol constants such as `LATEST_PROTOCOL_VERSION` and method-name constants stay in `core` for now) is finalized during implementation. `core`'s barrel keeps re-exporting the moved symbols so the ~hundreds of internal `core` imports don't all change. +- **Stays in `core`:** `Protocol`, transports, validators, error classes/enums, protocol constants, and `specTypeSchemas`/`isSpecType` (rebuilt from `sdk-shared`'s schemas, exported via `core/public` as today). + +### Public API surface after the change + +| Symbol kind | Canonical home | Also re-exported by | Typed as | +| --- | --- | --- | --- | +| Spec **types** (`Tool`, `CallToolResult`, …) | `sdk-shared` | `core/public`, `server`, `client` | TS types (Zod-free) | +| Zod **`*Schema` constants** | `sdk-shared` **only** | — (intentionally not on server/client) | real Zod schemas | +| `specTypeSchemas` / `isSpecType` | `core/public` | `server`, `client` | `StandardSchemaV1` (Zod-free) | + +Guidance: use `specTypeSchemas` for library-agnostic Standard Schema validation; import the Zod `*Schema` from `@modelcontextprotocol/sdk-shared` when you want Zod ergonomics (`.parse`, `.safeParse`, `.extend`, …) or are migrating v1 code. + +## Codemod changes + +Today: the `imports` transform sends `sdk/types.js` → `RESOLVE_BY_CONTEXT`; the `specSchemaAccess` transform rewrites the schema reference, converts `.safeParse()`, and emits a manual-migration diagnostic for `.parse()`. + +After: + +1. **`@modelcontextprotocol/sdk/types.js`** (and the extensionless `/types`, already handled) **→ `@modelcontextprotocol/sdk-shared`**: a fixed, context-free path swap covering both types and `*Schema` constants. Symbol names unchanged; existing `renamedSymbols` (e.g. `ResourceTemplate`→`ResourceTemplateType`) still apply. +2. **Retire `specSchemaAccess`'s schema rewriting.** Because `sdk-shared` exports real Zod schemas, `.parse()`/`.safeParse()`/`.extend()`/`.shape`/… all keep working untouched — no reference rename, no `.safeParse` result remap, no `.parse()` manual-migration diagnostic. The independent `schemaParamRemoval` transform (strips schema args from `request()`/`callTool()`) is unaffected and stays. +3. **`updatePackageJson` adds `@modelcontextprotocol/sdk-shared`** to the consumer whenever a `types.js` import is routed there. + +Expected effect on `firebase/firebase-tools`: the 4 `.parse()` errors disappear (schemas validate via Zod as before) and the project-type warnings on type-only files disappear (fixed target, no context resolution) → **zero codemod-introduced typecheck errors**, far fewer diagnostics. + +## Testing strategy + +- **`sdk-shared` package:** unit tests asserting expected exports exist; `barrelClean` test (no Node builtins); runtime-neutral. +- **`codemod`:** update `importPaths` tests (`types.js`/`/types` → `sdk-shared`); remove/trim `specSchemaAccess` tests; add coverage for the dependency addition and "schema usage passes through untouched." +- **`core`/`server`/`client`:** existing suites + typecheck stay green after the `types.ts` split (the main risk). +- **Batch test:** add `sdk-shared` to `LOCAL_PACKAGE_DIRS`; add an `overrides` entry so the transitive `server`→`sdk-shared` edge resolves to the local tarball; re-run `firebase/firebase-tools` and confirm 0 introduced typecheck errors. + +## Docs & rollout + +- Rewrite the spec-schema validation section in `docs/migration.md` and `docs/migration-SKILL.md`: schemas now import from `@modelcontextprotocol/sdk-shared`, `.parse`/`.safeParse` keep working; `specTypeSchemas` remains for library-agnostic validation. Document the new package. +- Add a changeset covering the new package and the codemod change. +- **PR #2277 coordination:** this **supersedes** #2277's `specTypeSchemas` type-only `parse`/`safeParse` approach. Its other improvements are independent and worth keeping: client/server inference (#2) and the `tasks/*` handler-map fix (#3). The extensionless-import fix (#4) is already implemented on this branch. + +## Risks & mitigations + +- **Splitting `types.ts`** is wide-reaching. Mitigation: keep `core`'s barrel re-exporting the moved symbols; land the move as its own step with full `core` typecheck/tests green before touching the codemod. +- **Transitive local-tarball resolution** in the batch test (`server`→`sdk-shared`). Mitigation: `overrides` entry pointing `sdk-shared` at the local tarball (or publish an alpha). +- **New publish/version target.** Mitigation: version `sdk-shared` in lockstep with the other v2 packages via changesets. + +## Open questions (non-blocking) + +- Final split boundary inside `types.ts` (which non-schema symbols, if any, also belong in `sdk-shared`). +- Whether `specTypeSchemas`/`isSpecType` should eventually move to `sdk-shared` too (deferred). diff --git a/packages/codemod/batch-test/repos.json b/packages/codemod/batch-test/repos.json index d7faa64e2c..28dbc4348d 100644 --- a/packages/codemod/batch-test/repos.json +++ b/packages/codemod/batch-test/repos.json @@ -1,16 +1,16 @@ [ { - "repo": "upstash/context7", - "ref": "master", + "repo": "firebase/firebase-tools", + "ref": "main", "packages": [ { - "dir": "packages/mcp", - "sourceDir": "src", + "dir": ".", + "sourceDir": "src/mcp", "checks": { - "typecheck": "pnpm run typecheck", - "build": "pnpm run build", - "test": "pnpm run test", - "lint": "pnpm run lint" + "typecheck": "npx tsc -p tsconfig.compile.json", + "build": null, + "test": null, + "lint": null } } ] diff --git a/packages/codemod/package.json b/packages/codemod/package.json index 264f973ac6..6c2c290457 100644 --- a/packages/codemod/package.json +++ b/packages/codemod/package.json @@ -35,8 +35,7 @@ "scripts": { "typecheck": "tsgo -p tsconfig.json --noEmit", "generate:versions": "tsx scripts/generateVersions.ts", - "generate:spec-schemas": "tsx scripts/generateSpecSchemaMap.ts", - "prebuild": "pnpm run generate:versions && pnpm run generate:spec-schemas", + "prebuild": "pnpm run generate:versions", "build": "tsdown", "build:watch": "tsdown --watch", "prepack": "pnpm run build", diff --git a/packages/codemod/scripts/generateSpecSchemaMap.ts b/packages/codemod/scripts/generateSpecSchemaMap.ts deleted file mode 100644 index 29796f62ab..0000000000 --- a/packages/codemod/scripts/generateSpecSchemaMap.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { readFileSync, writeFileSync } from 'node:fs'; -import path from 'node:path'; -import { fileURLToPath } from 'node:url'; - -const __dirname = path.dirname(fileURLToPath(import.meta.url)); -const specTypeSchemaPath = path.resolve(__dirname, '../../core/src/types/specTypeSchema.ts'); - -const source = readFileSync(specTypeSchemaPath, 'utf8'); - -// Extract SPEC_SCHEMA_KEYS array entries -const keysMatch = source.match(/const SPEC_SCHEMA_KEYS = \[([\s\S]*?)\] as const/); -if (!keysMatch) throw new Error('Could not find SPEC_SCHEMA_KEYS in specTypeSchema.ts'); - -const protocolSchemas = [...keysMatch[1]!.matchAll(/'([^']+)'/g)].map(m => m[1]!); - -// Extract auth schema keys -const authMatch = source.match(/const authSchemas = \{([\s\S]*?)\} as const/); -if (!authMatch) throw new Error('Could not find authSchemas in specTypeSchema.ts'); - -const authSchemas = [...authMatch[1]!.matchAll(/(\w+Schema)/g)].map(m => m[1]!); - -const allSchemas = [...protocolSchemas, ...authSchemas].toSorted(); - -const entries = allSchemas.map((s, i) => ` '${s}'${i < allSchemas.length - 1 ? ',' : ''}`).join('\n'); - -const output = `// AUTO-GENERATED — do not edit. Run \`pnpm run generate:spec-schemas\` to regenerate. -export const SPEC_SCHEMA_NAMES: ReadonlySet = new Set([ -${entries} -]); - -export function specSchemaToTypeName(schemaName: string): string | undefined { - if (!SPEC_SCHEMA_NAMES.has(schemaName)) return undefined; - return schemaName.slice(0, -'Schema'.length); -} -`; - -const outPath = path.resolve(__dirname, '../src/generated/specSchemaMap.ts'); -writeFileSync(outPath, output); -console.log(`Wrote ${outPath} (${allSchemas.length} schemas)`); diff --git a/packages/codemod/scripts/generateVersions.ts b/packages/codemod/scripts/generateVersions.ts index 3a77b592bf..fa37e2c273 100644 --- a/packages/codemod/scripts/generateVersions.ts +++ b/packages/codemod/scripts/generateVersions.ts @@ -10,7 +10,8 @@ const PACKAGE_DIRS: Record = { '@modelcontextprotocol/server': 'server', '@modelcontextprotocol/node': 'middleware/node', '@modelcontextprotocol/express': 'middleware/express', - '@modelcontextprotocol/server-legacy': 'server-legacy' + '@modelcontextprotocol/server-legacy': 'server-legacy', + '@modelcontextprotocol/sdk-shared': 'sdk-shared' }; const versions: Record = {}; diff --git a/packages/codemod/src/bin/batchTest.ts b/packages/codemod/src/bin/batchTest.ts index 0e45e1715b..eefe0f2df5 100644 --- a/packages/codemod/src/bin/batchTest.ts +++ b/packages/codemod/src/bin/batchTest.ts @@ -88,6 +88,7 @@ const LOCAL_PACKAGE_DIRS: Record = { '@modelcontextprotocol/core': path.join(SDK_ROOT, 'packages/core'), '@modelcontextprotocol/server': path.join(SDK_ROOT, 'packages/server'), '@modelcontextprotocol/server-legacy': path.join(SDK_ROOT, 'packages/server-legacy'), + '@modelcontextprotocol/sdk-shared': path.join(SDK_ROOT, 'packages/sdk-shared'), '@modelcontextprotocol/express': path.join(SDK_ROOT, 'packages/middleware/express'), '@modelcontextprotocol/fastify': path.join(SDK_ROOT, 'packages/middleware/fastify'), '@modelcontextprotocol/hono': path.join(SDK_ROOT, 'packages/middleware/hono'), diff --git a/packages/codemod/src/generated/specSchemaMap.ts b/packages/codemod/src/generated/specSchemaMap.ts deleted file mode 100644 index 77f3d3dfc8..0000000000 --- a/packages/codemod/src/generated/specSchemaMap.ts +++ /dev/null @@ -1,173 +0,0 @@ -// AUTO-GENERATED — do not edit. Run `pnpm run generate:spec-schemas` to regenerate. -export const SPEC_SCHEMA_NAMES: ReadonlySet = new Set([ - 'AnnotationsSchema', - 'AudioContentSchema', - 'BaseMetadataSchema', - 'BlobResourceContentsSchema', - 'BooleanSchemaSchema', - 'CallToolRequestParamsSchema', - 'CallToolRequestSchema', - 'CallToolResultSchema', - 'CancelTaskRequestSchema', - 'CancelTaskResultSchema', - 'CancelledNotificationParamsSchema', - 'CancelledNotificationSchema', - 'ClientCapabilitiesSchema', - 'ClientNotificationSchema', - 'ClientRequestSchema', - 'ClientResultSchema', - 'CompatibilityCallToolResultSchema', - 'CompleteRequestParamsSchema', - 'CompleteRequestSchema', - 'CompleteResultSchema', - 'ContentBlockSchema', - 'CreateMessageRequestParamsSchema', - 'CreateMessageRequestSchema', - 'CreateMessageResultSchema', - 'CreateMessageResultWithToolsSchema', - 'CreateTaskResultSchema', - 'CursorSchema', - 'DiscoverRequestSchema', - 'DiscoverResultSchema', - 'ElicitRequestFormParamsSchema', - 'ElicitRequestParamsSchema', - 'ElicitRequestSchema', - 'ElicitRequestURLParamsSchema', - 'ElicitResultSchema', - 'ElicitationCompleteNotificationParamsSchema', - 'ElicitationCompleteNotificationSchema', - 'EmbeddedResourceSchema', - 'EmptyResultSchema', - 'EnumSchemaSchema', - 'GetPromptRequestParamsSchema', - 'GetPromptRequestSchema', - 'GetPromptResultSchema', - 'GetTaskPayloadRequestSchema', - 'GetTaskPayloadResultSchema', - 'GetTaskRequestSchema', - 'GetTaskResultSchema', - 'IconSchema', - 'IconsSchema', - 'ImageContentSchema', - 'ImplementationSchema', - 'InitializeRequestParamsSchema', - 'InitializeRequestSchema', - 'InitializeResultSchema', - 'InitializedNotificationSchema', - 'JSONArraySchema', - 'JSONObjectSchema', - 'JSONRPCErrorResponseSchema', - 'JSONRPCMessageSchema', - 'JSONRPCNotificationSchema', - 'JSONRPCRequestSchema', - 'JSONRPCResponseSchema', - 'JSONRPCResultResponseSchema', - 'JSONValueSchema', - 'LegacyTitledEnumSchemaSchema', - 'ListPromptsRequestSchema', - 'ListPromptsResultSchema', - 'ListResourceTemplatesRequestSchema', - 'ListResourceTemplatesResultSchema', - 'ListResourcesRequestSchema', - 'ListResourcesResultSchema', - 'ListRootsRequestSchema', - 'ListRootsResultSchema', - 'ListTasksRequestSchema', - 'ListTasksResultSchema', - 'ListToolsRequestSchema', - 'ListToolsResultSchema', - 'LoggingLevelSchema', - 'LoggingMessageNotificationParamsSchema', - 'LoggingMessageNotificationSchema', - 'ModelHintSchema', - 'ModelPreferencesSchema', - 'MultiSelectEnumSchemaSchema', - 'NotificationSchema', - 'NumberSchemaSchema', - 'OAuthClientInformationFullSchema', - 'OAuthClientInformationSchema', - 'OAuthClientMetadataSchema', - 'OAuthClientRegistrationErrorSchema', - 'OAuthErrorResponseSchema', - 'OAuthMetadataSchema', - 'OAuthProtectedResourceMetadataSchema', - 'OAuthTokenRevocationRequestSchema', - 'OAuthTokensSchema', - 'OpenIdProviderDiscoveryMetadataSchema', - 'OpenIdProviderMetadataSchema', - 'PaginatedRequestParamsSchema', - 'PaginatedRequestSchema', - 'PaginatedResultSchema', - 'PingRequestSchema', - 'PrimitiveSchemaDefinitionSchema', - 'ProgressNotificationParamsSchema', - 'ProgressNotificationSchema', - 'ProgressSchema', - 'ProgressTokenSchema', - 'PromptArgumentSchema', - 'PromptListChangedNotificationSchema', - 'PromptMessageSchema', - 'PromptReferenceSchema', - 'PromptSchema', - 'ReadResourceRequestParamsSchema', - 'ReadResourceRequestSchema', - 'ReadResourceResultSchema', - 'RelatedTaskMetadataSchema', - 'RequestIdSchema', - 'RequestMetaEnvelopeSchema', - 'RequestMetaSchema', - 'RequestSchema', - 'ResourceContentsSchema', - 'ResourceLinkSchema', - 'ResourceListChangedNotificationSchema', - 'ResourceRequestParamsSchema', - 'ResourceSchema', - 'ResourceTemplateReferenceSchema', - 'ResourceTemplateSchema', - 'ResourceUpdatedNotificationParamsSchema', - 'ResourceUpdatedNotificationSchema', - 'ResultSchema', - 'RoleSchema', - 'RootSchema', - 'RootsListChangedNotificationSchema', - 'SamplingContentSchema', - 'SamplingMessageContentBlockSchema', - 'SamplingMessageSchema', - 'ServerCapabilitiesSchema', - 'ServerNotificationSchema', - 'ServerRequestSchema', - 'ServerResultSchema', - 'SetLevelRequestParamsSchema', - 'SetLevelRequestSchema', - 'SingleSelectEnumSchemaSchema', - 'StringSchemaSchema', - 'SubscribeRequestParamsSchema', - 'SubscribeRequestSchema', - 'TaskAugmentedRequestParamsSchema', - 'TaskCreationParamsSchema', - 'TaskMetadataSchema', - 'TaskSchema', - 'TaskStatusNotificationParamsSchema', - 'TaskStatusNotificationSchema', - 'TaskStatusSchema', - 'TextContentSchema', - 'TextResourceContentsSchema', - 'TitledMultiSelectEnumSchemaSchema', - 'TitledSingleSelectEnumSchemaSchema', - 'ToolAnnotationsSchema', - 'ToolChoiceSchema', - 'ToolExecutionSchema', - 'ToolListChangedNotificationSchema', - 'ToolResultContentSchema', - 'ToolSchema', - 'ToolUseContentSchema', - 'UnsubscribeRequestParamsSchema', - 'UnsubscribeRequestSchema', - 'UntitledMultiSelectEnumSchemaSchema', - 'UntitledSingleSelectEnumSchemaSchema' -]); - -export function specSchemaToTypeName(schemaName: string): string | undefined { - if (!SPEC_SCHEMA_NAMES.has(schemaName)) return undefined; - return schemaName.slice(0, -'Schema'.length); -} diff --git a/packages/codemod/src/generated/versions.ts b/packages/codemod/src/generated/versions.ts index 7166154021..afcef6e1ed 100644 --- a/packages/codemod/src/generated/versions.ts +++ b/packages/codemod/src/generated/versions.ts @@ -4,5 +4,6 @@ export const V2_PACKAGE_VERSIONS: Record = { '@modelcontextprotocol/server': '^2.0.0-alpha.2', '@modelcontextprotocol/node': '^2.0.0-alpha.2', '@modelcontextprotocol/express': '^2.0.0-alpha.2', - '@modelcontextprotocol/server-legacy': '^2.0.0-alpha.2' + '@modelcontextprotocol/server-legacy': '^2.0.0-alpha.2', + '@modelcontextprotocol/sdk-shared': '^2.0.0-alpha.0' }; diff --git a/packages/codemod/src/migrations/v1-to-v2/mappings/importMap.ts b/packages/codemod/src/migrations/v1-to-v2/mappings/importMap.ts index bf229772b0..04366c7dd4 100644 --- a/packages/codemod/src/migrations/v1-to-v2/mappings/importMap.ts +++ b/packages/codemod/src/migrations/v1-to-v2/mappings/importMap.ts @@ -4,6 +4,13 @@ export interface ImportMapping { renamedSymbols?: Record; /** Route specific symbols to a different target package than `target`. */ symbolTargetOverrides?: Record; + /** + * Route every imported symbol whose name ends in `Schema` to this package, instead of `target`. + * Used for `sdk/types.js`: the spec Zod schemas now live in `@modelcontextprotocol/sdk-shared` + * (so `Schema.parse(...)` keeps working), while the spec types/constants resolve by context. + * `symbolTargetOverrides` (exact-name) takes precedence over this suffix rule. + */ + schemaSymbolTarget?: string; removalMessage?: string; /** No entries currently set this; scaffolding for when a v1 symbol has no v2 equivalent yet. */ isV2Gap?: boolean; @@ -119,6 +126,7 @@ export const IMPORT_MAP: Record = { '@modelcontextprotocol/sdk/types.js': { target: 'RESOLVE_BY_CONTEXT', status: 'moved', + schemaSymbolTarget: '@modelcontextprotocol/sdk-shared', renamedSymbols: { ResourceTemplate: 'ResourceTemplateType' } diff --git a/packages/codemod/src/migrations/v1-to-v2/transforms/importPaths.ts b/packages/codemod/src/migrations/v1-to-v2/transforms/importPaths.ts index 14c63f9233..1215634460 100644 --- a/packages/codemod/src/migrations/v1-to-v2/transforms/importPaths.ts +++ b/packages/codemod/src/migrations/v1-to-v2/transforms/importPaths.ts @@ -1,10 +1,12 @@ import type { SourceFile } from 'ts-morph'; +import { SyntaxKind } from 'ts-morph'; import type { Diagnostic, Transform, TransformContext, TransformResult } from '../../../types.js'; import { renameAllReferences } from '../../../utils/astUtils.js'; import { actionRequired, info, v2Gap, warning } from '../../../utils/diagnostics.js'; import { addOrMergeImport, getSdkExports, getSdkImports, isTypeOnlyImport } from '../../../utils/importUtils.js'; import { resolveTypesPackage } from '../../../utils/projectAnalyzer.js'; +import type { ImportMapping } from '../mappings/importMap.js'; import { isAuthImport, lookupImportMapping } from '../mappings/importMap.js'; import { SIMPLE_RENAMES } from '../mappings/symbolMap.js'; @@ -17,6 +19,21 @@ const REEXPORT_WARNINGS: Record = { 'Re-exported StreamableHTTPError was renamed to SdkHttpError in v2 with a different constructor. Update this re-export manually.' }; +/** + * The per-symbol target package for a symbol imported/re-exported from `mapping`'s module, or + * `undefined` when the symbol should use the mapping's resolved `target`. Exact-name + * `symbolTargetOverrides` win over the `schemaSymbolTarget` (`*Schema`) suffix rule. + */ +function symbolTargetOverride(name: string, mapping: ImportMapping): string | undefined { + if (mapping.symbolTargetOverrides && name in mapping.symbolTargetOverrides) { + return mapping.symbolTargetOverrides[name]; + } + if (mapping.schemaSymbolTarget && name.endsWith('Schema')) { + return mapping.schemaSymbolTarget; + } + return undefined; +} + export const importPathsTransform: Transform = { name: 'Import path rewrites', id: 'imports', @@ -119,11 +136,13 @@ export const importPathsTransform: Transform = { const hasAlias = namedImports.some(n => n.getAliasNode() !== undefined); if (defaultImport || namespaceImport || hasAlias) { let effectiveTarget = targetPackage; - if (mapping.symbolTargetOverrides && !namespaceImport && !defaultImport) { - const allOverridden = namedImports.length > 0 && namedImports.every(n => n.getName() in mapping.symbolTargetOverrides!); - if (allOverridden) { - effectiveTarget = mapping.symbolTargetOverrides[namedImports[0]!.getName()]!; - } else if (namedImports.some(n => n.getName() in mapping.symbolTargetOverrides!)) { + if ((mapping.symbolTargetOverrides || mapping.schemaSymbolTarget) && !namespaceImport && !defaultImport) { + const overrides = namedImports.map(n => symbolTargetOverride(n.getName(), mapping)); + const uniqueOverrides = new Set(overrides.filter((t): t is string => t !== undefined)); + const allOverridden = namedImports.length > 0 && overrides.every(t => t !== undefined); + if (allOverridden && uniqueOverrides.size === 1) { + effectiveTarget = [...uniqueOverrides][0]!; + } else if (uniqueOverrides.size > 0) { diagnostics.push( actionRequired( filePath, @@ -134,6 +153,29 @@ export const importPathsTransform: Transform = { ); } } + // A namespace import (`import * as ns from '…/types.js'`) cannot be split per-symbol, so + // any `ns.Schema` accesses would silently resolve against the wrong package. Flag them. + if (namespaceImport && mapping.schemaSymbolTarget) { + const nsName = namespaceImport.getText(); + const schemaNames = [ + ...new Set( + sourceFile + .getDescendantsOfKind(SyntaxKind.PropertyAccessExpression) + .filter(pa => pa.getExpression().getText() === nsName && pa.getName().endsWith('Schema')) + .map(pa => pa.getName()) + ) + ]; + if (schemaNames.length > 0) { + diagnostics.push( + actionRequired( + filePath, + imp, + `Namespace import of ${specifier} is used to access Zod schema(s) (${schemaNames.join(', ')}) that moved to ${mapping.schemaSymbolTarget}. ` + + `Import them with a named import (e.g. \`import { ${schemaNames[0]} } from '${mapping.schemaSymbolTarget}'\`) and update the qualified usages.` + ) + ); + } + } usedPackages.add(effectiveTarget); imp.setModuleSpecifier(effectiveTarget); if (mapping.renamedSymbols) { @@ -168,7 +210,7 @@ export const importPathsTransform: Transform = { const name = n.getName(); const resolvedName = mapping.renamedSymbols?.[name] ?? name; const specifierTypeOnly = typeOnly || n.isTypeOnly(); - const symbolTarget = mapping.symbolTargetOverrides?.[name] ?? targetPackage; + const symbolTarget = symbolTargetOverride(name, mapping) ?? targetPackage; usedPackages.add(symbolTarget); addPending(symbolTarget, [resolvedName], specifierTypeOnly); } @@ -262,12 +304,14 @@ function rewriteExportDeclarations( } } - if (mapping.symbolTargetOverrides) { + if (mapping.symbolTargetOverrides || mapping.schemaSymbolTarget) { const namedExports = exp.getNamedExports(); - const allOverridden = namedExports.length > 0 && namedExports.every(s => s.getName() in mapping.symbolTargetOverrides!); - if (allOverridden) { - targetPackage = mapping.symbolTargetOverrides[namedExports[0]!.getName()]!; - } else if (namedExports.some(s => s.getName() in mapping.symbolTargetOverrides!)) { + const overrides = namedExports.map(s => symbolTargetOverride(s.getName(), mapping)); + const uniqueOverrides = new Set(overrides.filter((t): t is string => t !== undefined)); + const allOverridden = namedExports.length > 0 && overrides.every(t => t !== undefined); + if (allOverridden && uniqueOverrides.size === 1) { + targetPackage = [...uniqueOverrides][0]!; + } else if (uniqueOverrides.size > 0) { diagnostics.push( actionRequired( filePath, diff --git a/packages/codemod/src/migrations/v1-to-v2/transforms/index.ts b/packages/codemod/src/migrations/v1-to-v2/transforms/index.ts index 7b6b54b28b..bdc1c5e6ba 100644 --- a/packages/codemod/src/migrations/v1-to-v2/transforms/index.ts +++ b/packages/codemod/src/migrations/v1-to-v2/transforms/index.ts @@ -7,7 +7,6 @@ import { mcpServerApiTransform } from './mcpServerApi.js'; import { mockPathsTransform } from './mockPaths.js'; import { removedApisTransform } from './removedApis.js'; import { schemaParamRemovalTransform } from './schemaParamRemoval.js'; -import { specSchemaAccessTransform } from './specSchemaAccess.js'; import { symbolRenamesTransform } from './symbolRenames.js'; // Ordering matters — do not reorder without understanding dependencies: @@ -31,11 +30,7 @@ import { symbolRenamesTransform } from './symbolRenames.js'; // 5. handlerRegistration, schemaParamRemoval, and expressMiddleware are // independent of each other but all depend on importPaths having run. // -// 6. specSchemaAccess runs after handlerRegistration and schemaParamRemoval: -// those transforms remove spec schema references they handle. specSchemaAccess -// then processes remaining standalone usages (safeParse, parse, z.infer, etc.). -// -// 7. mockPaths runs last: handles test mocks and dynamic imports, +// 6. mockPaths runs last: handles test mocks and dynamic imports, // independent of the other transforms. export const v1ToV2Transforms: Transform[] = [ importPathsTransform, @@ -44,7 +39,6 @@ export const v1ToV2Transforms: Transform[] = [ mcpServerApiTransform, handlerRegistrationTransform, schemaParamRemovalTransform, - specSchemaAccessTransform, expressMiddlewareTransform, contextTypesTransform, mockPathsTransform diff --git a/packages/codemod/src/migrations/v1-to-v2/transforms/specSchemaAccess.ts b/packages/codemod/src/migrations/v1-to-v2/transforms/specSchemaAccess.ts deleted file mode 100644 index 79f4a0a707..0000000000 --- a/packages/codemod/src/migrations/v1-to-v2/transforms/specSchemaAccess.ts +++ /dev/null @@ -1,392 +0,0 @@ -import type { SourceFile } from 'ts-morph'; -import { Node, SyntaxKind } from 'ts-morph'; - -import { SPEC_SCHEMA_NAMES, specSchemaToTypeName } from '../../../generated/specSchemaMap.js'; -import type { Diagnostic, Transform, TransformContext, TransformResult } from '../../../types.js'; -import { isKeyPositionIdentifier } from '../../../utils/astUtils.js'; -import { actionRequired, warning } from '../../../utils/diagnostics.js'; -import { addOrMergeImport, isAnyMcpSpecifier, removeUnusedImport } from '../../../utils/importUtils.js'; - -export const specSchemaAccessTransform: Transform = { - name: 'Spec schema standalone usage', - id: 'spec-schemas', - apply(sourceFile: SourceFile, _context: TransformContext): TransformResult { - const diagnostics: Diagnostic[] = []; - let changesCount = 0; - - const schemaImports = collectSpecSchemaImports(sourceFile); - if (schemaImports.size === 0) return { changesCount: 0, diagnostics: [] }; - - for (const [localName, originalName] of schemaImports) { - const typeName = specSchemaToTypeName(originalName); - if (!typeName) continue; - - const refs = findNonImportReferences(sourceFile, localName); - if (refs.length === 0) continue; - - for (const ref of refs) { - const result = handleReference(ref, localName, typeName, sourceFile, diagnostics); - if (result) changesCount++; - } - removeUnusedImport(sourceFile, localName, true); - } - - return { changesCount, diagnostics }; - } -}; - -function collectSpecSchemaImports(sourceFile: SourceFile): Map { - const result = new Map(); - for (const imp of sourceFile.getImportDeclarations()) { - if (!isAnyMcpSpecifier(imp.getModuleSpecifierValue())) continue; - for (const n of imp.getNamedImports()) { - const exportName = n.getName(); - if (!SPEC_SCHEMA_NAMES.has(exportName)) continue; - const localName = n.getAliasNode()?.getText() ?? exportName; - result.set(localName, exportName); - } - } - return result; -} - -function findNonImportReferences(sourceFile: SourceFile, localName: string): import('ts-morph').Node[] { - const refs: import('ts-morph').Node[] = []; - sourceFile.forEachDescendant(node => { - if (!Node.isIdentifier(node)) return; - if (node.getText() !== localName) return; - const parent = node.getParent(); - if (parent && Node.isImportSpecifier(parent)) return; - refs.push(node); - }); - return refs; -} - -function handleReference( - ref: import('ts-morph').Node, - localName: string, - typeName: string, - sourceFile: SourceFile, - diagnostics: Diagnostic[] -): boolean { - // Pattern: z.infer — type position - if (isTypeofInTypePosition(ref)) { - diagnostics.push( - actionRequired( - sourceFile.getFilePath(), - ref, - `Replace \`z.infer\` with the \`${typeName}\` type (already exported from the same v2 package).` - ) - ); - return false; - } - - // Pattern: XSchema.safeParse(v).success — auto-transform to isSpecType.X(v) - if (isSafeParseSuccessPattern(ref)) { - const safeParseAccess = ref.getParent() as import('ts-morph').PropertyAccessExpression; - const safeParseCall = safeParseAccess.getParent() as import('ts-morph').CallExpression; - const successAccess = safeParseCall.getParent() as import('ts-morph').PropertyAccessExpression; - const args = safeParseCall.getArguments(); - const argText = args.length > 0 ? args[0]!.getText() : ''; - successAccess.replaceWithText(`isSpecType.${typeName}(${argText})`); - ensureImport(sourceFile, 'isSpecType'); - return true; - } - - // Pattern: const x = XSchema.safeParse(v) — auto-transform when result is captured in a variable - if (isSafeParsePattern(ref)) { - const safeParseAccess = ref.getParent() as import('ts-morph').PropertyAccessExpression; - const safeParseCall = safeParseAccess.getParent() as import('ts-morph').CallExpression; - - if (isCapturedSafeParsePattern(safeParseCall)) { - return rewriteCapturedSafeParse(safeParseCall, localName, typeName, sourceFile, diagnostics); - } - - return rewriteUnsupportedSchemaCall(ref, safeParseCall, localName, typeName, 'safeParse', sourceFile, diagnostics); - } - - // Pattern: XSchema.parse(v) — rewrite to the StandardSchema validate() primitive (or, when the - // result is used, swap the identifier) so we never leave behind an import of a non-exported schema. - if (isParsePattern(ref)) { - const parseAccess = ref.getParent() as import('ts-morph').PropertyAccessExpression; - const parseCall = parseAccess.getParent() as import('ts-morph').CallExpression; - return rewriteUnsupportedSchemaCall(ref, parseCall, localName, typeName, 'parse', sourceFile, diagnostics); - } - - // Pattern: XSchema used as value (function arg, assignment, etc.) - const parent = ref.getParent(); - if (parent && Node.isPropertyAccessExpression(parent) && parent.getExpression() === ref) { - const line = ref.getStartLineNumber(); - ref.replaceWithText(`specTypeSchemas.${typeName}`); - ensureImport(sourceFile, 'specTypeSchemas'); - diagnostics.push( - warning( - sourceFile.getFilePath(), - line, - `Replaced ${localName} with specTypeSchemas.${typeName}. Note: typed as StandardSchemaV1, not ZodType — Zod methods like .safeParse()/.parse()/.parseAsync() are not available. Manual rewrite required.` - ) - ); - return true; - } - - if (parent && Node.isExportSpecifier(parent)) { - diagnostics.push( - actionRequired( - sourceFile.getFilePath(), - ref, - `Re-export of ${localName} requires manual update: replace with specTypeSchemas.${typeName} or remove.` - ) - ); - return false; - } - - if (parent && Node.isShorthandPropertyAssignment(parent)) { - const line = ref.getStartLineNumber(); - parent.replaceWithText(`'${localName}': specTypeSchemas.${typeName}`); - ensureImport(sourceFile, 'specTypeSchemas'); - diagnostics.push( - warning( - sourceFile.getFilePath(), - line, - `Replaced ${localName} with specTypeSchemas.${typeName}. Note: typed as StandardSchemaV1, not ZodType — Zod methods like .safeParse()/.parse() are not available.` - ) - ); - return true; - } - - if (parent && isKeyPositionIdentifier(ref)) { - return false; - } - - // Value position: replace identifier with specTypeSchemas.X - const line = ref.getStartLineNumber(); - ref.replaceWithText(`specTypeSchemas.${typeName}`); - ensureImport(sourceFile, 'specTypeSchemas'); - diagnostics.push( - warning( - sourceFile.getFilePath(), - line, - `Replaced ${localName} with specTypeSchemas.${typeName}. Note: typed as StandardSchemaV1, not ZodType — Zod methods like .safeParse()/.parse() are not available.` - ) - ); - return true; -} - -function isSafeParseSuccessPattern(ref: import('ts-morph').Node): boolean { - const parent = ref.getParent(); - if (!parent || !Node.isPropertyAccessExpression(parent)) return false; - if (parent.getName() !== 'safeParse' || parent.getExpression() !== ref) return false; - const grandParent = parent.getParent(); - if (!grandParent || !Node.isCallExpression(grandParent)) return false; - const greatGrandParent = grandParent.getParent(); - if (!greatGrandParent || !Node.isPropertyAccessExpression(greatGrandParent)) return false; - return greatGrandParent.getName() === 'success'; -} - -function isSafeParsePattern(ref: import('ts-morph').Node): boolean { - const parent = ref.getParent(); - if (!parent || !Node.isPropertyAccessExpression(parent)) return false; - if (parent.getName() !== 'safeParse' || parent.getExpression() !== ref) return false; - const grandParent = parent.getParent(); - return !!grandParent && Node.isCallExpression(grandParent); -} - -function isParsePattern(ref: import('ts-morph').Node): boolean { - const parent = ref.getParent(); - if (!parent || !Node.isPropertyAccessExpression(parent)) return false; - if (parent.getName() !== 'parse' || parent.getExpression() !== ref) return false; - const grandParent = parent.getParent(); - return !!grandParent && Node.isCallExpression(grandParent); -} - -function isTypeofInTypePosition(ref: import('ts-morph').Node): boolean { - const parent = ref.getParent(); - if (!parent) return false; - return Node.isTypeQuery(parent); -} - -/** - * Checks if a safeParse call result is captured in a `const` variable declaration. - * Pattern: `const x = Schema.safeParse(v);` - */ -function isCapturedSafeParsePattern(safeParseCall: import('ts-morph').CallExpression): boolean { - const parent = safeParseCall.getParent(); - if (!parent || !Node.isVariableDeclaration(parent)) return false; - const nameNode = parent.getNameNode(); - if (!Node.isIdentifier(nameNode)) return false; - const declList = parent.getParent(); - if (!declList || !Node.isVariableDeclarationList(declList)) return false; - const flags = declList.getDeclarationKind(); - return flags === 'const' || flags === 'let'; -} - -/** - * Rewrites a captured safeParse pattern: - * const x = Schema.safeParse(v) → const x = specTypeSchemas.T['~standard'].validate(v) - * x.success → x.issues === undefined - * x.data → x.value - * x.error → x.issues - */ -function rewriteCapturedSafeParse( - safeParseCall: import('ts-morph').CallExpression, - localName: string, - typeName: string, - sourceFile: SourceFile, - diagnostics: Diagnostic[] -): boolean { - const varDecl = safeParseCall.getParent() as import('ts-morph').VariableDeclaration; - const varName = varDecl.getName(); - - const args = safeParseCall.getArguments(); - const argText = args.length > 0 ? args[0]!.getText() : ''; - - // Rewrite the safeParse call - safeParseCall.replaceWithText(`specTypeSchemas.${typeName}['~standard'].validate(${argText})`); - ensureImport(sourceFile, 'specTypeSchemas'); - - // Find and rewrite all property accesses on the result variable (scoped to declaring block) - const replacements: { node: import('ts-morph').Node; newText: string }[] = []; - const scope = varDecl.getFirstAncestorByKind(SyntaxKind.Block) ?? sourceFile; - scope.forEachDescendant(node => { - if (!Node.isPropertyAccessExpression(node)) return; - const expr = node.getExpression(); - if (!Node.isIdentifier(expr) || expr.getText() !== varName) return; - - const propName = node.getName(); - switch (propName) { - case 'success': { - // Check for !x.success → x.issues !== undefined - const parentNode = node.getParent(); - if ( - parentNode && - Node.isPrefixUnaryExpression(parentNode) && - parentNode.getOperatorToken() === SyntaxKind.ExclamationToken - ) { - replacements.push({ node: parentNode, newText: `${varName}.issues !== undefined` }); - } else { - replacements.push({ node, newText: `(${varName}.issues === undefined)` }); - } - break; - } - case 'data': { - replacements.push({ node, newText: `${varName}.value` }); - break; - } - case 'error': { - const errorParent = node.getParent(); - if (errorParent && Node.isPropertyAccessExpression(errorParent) && errorParent.getExpression() === node) { - const subProp = errorParent.getName(); - if (subProp === 'issues') { - replacements.push({ node: errorParent, newText: `${varName}.issues` }); - } else if (subProp === 'message') { - replacements.push({ node: errorParent, newText: `${varName}.issues?.map(i => i.message).join(', ')` }); - } else { - diagnostics.push( - actionRequired( - sourceFile.getFilePath(), - errorParent, - `${varName}.error.${subProp} has no StandardSchema equivalent. Manual migration required.` - ) - ); - } - } else { - replacements.push({ node, newText: `${varName}.issues` }); - } - break; - } - } - }); - - // Apply in reverse order to avoid position shifts - const sorted = replacements.toSorted((a, b) => b.node.getStart() - a.node.getStart()); - for (const { node, newText } of sorted) { - node.replaceWithText(newText); - } - - diagnostics.push( - warning( - sourceFile.getFilePath(), - varDecl.getStartLineNumber(), - `Rewrote ${localName}.safeParse() to specTypeSchemas.${typeName}['~standard'].validate(). ` + - `Result properties remapped: .success → .issues === undefined, .data → .value, .error → .issues.` - ) - ); - - return true; -} - -/** - * Handles spec-schema usages that have no behavior-preserving v2 equivalent: the Zod-only - * methods `.parse()` and (uncaptured) `.safeParse()`. In v2 these schemas are StandardSchemaV1 - * values that are NOT named public exports, so leaving the original import in place produces an - * unresolved-import error (e.g. `PromptSchema` is not exported by `@modelcontextprotocol/server`). - * - * - Result discarded (validation for side-effect only): rewrite `XSchema.parse(v)` → - * `specTypeSchemas.T['~standard'].validate(v)` so the code compiles. NOTE: `validate()` does not - * throw, so `.parse()`'s throw-on-invalid behavior is lost — flagged via an actionRequired comment. - * - Result used: swap only the identifier to `specTypeSchemas.T` so the import resolves; the - * `.parse()`/`.safeParse()` call and its result shape still need a manual fix (flagged). - * - * Either way the original (now non-exported) schema import is dropped by the caller's - * removeUnusedImport, so no dangling import survives. - */ -function rewriteUnsupportedSchemaCall( - ref: import('ts-morph').Node, - callNode: import('ts-morph').CallExpression, - localName: string, - typeName: string, - method: 'parse' | 'safeParse', - sourceFile: SourceFile, - diagnostics: Diagnostic[] -): boolean { - const resultDiscarded = Node.isExpressionStatement(callNode.getParent()); - - if (resultDiscarded) { - const argText = callNode - .getArguments() - .map(a => a.getText()) - .join(', '); - const semantics = - method === 'parse' - ? 'validate() does NOT throw on invalid input (parse() did) — if you relied on that, add `if (result.issues) throw …`.' - : 'the result shape changed from { success, data, error } to { value, issues }.'; - diagnostics.push( - actionRequired( - sourceFile.getFilePath(), - callNode, - `Rewrote ${localName}.${method}() to specTypeSchemas.${typeName}['~standard'].validate(): ` + - `v2 spec schemas are StandardSchemaV1, not Zod. Note: ${semantics}` - ) - ); - callNode.replaceWithText(`specTypeSchemas.${typeName}['~standard'].validate(${argText})`); - ensureImport(sourceFile, 'specTypeSchemas'); - return true; - } - - diagnostics.push( - actionRequired( - sourceFile.getFilePath(), - ref, - `${localName}.${method}() is not available on v2 spec schemas (StandardSchemaV1, not Zod). ` + - `Replaced ${localName} with specTypeSchemas.${typeName}; rewrite the .${method}(...) call using ` + - `specTypeSchemas.${typeName}['~standard'].validate(...) (returns { value, issues }, does not throw).` - ) - ); - ref.replaceWithText(`specTypeSchemas.${typeName}`); - ensureImport(sourceFile, 'specTypeSchemas'); - return true; -} - -function ensureImport(sourceFile: SourceFile, symbol: string): void { - const existingImport = sourceFile.getImportDeclarations().find(imp => { - if (!isAnyMcpSpecifier(imp.getModuleSpecifierValue())) return false; - return imp.getNamedImports().some(n => n.getName() === symbol); - }); - if (existingImport) return; - - const targetPkg = sourceFile.getImportDeclarations().find(imp => { - const spec = imp.getModuleSpecifierValue(); - return spec === '@modelcontextprotocol/server' || spec === '@modelcontextprotocol/client'; - }); - const target = targetPkg?.getModuleSpecifierValue() ?? '@modelcontextprotocol/server'; - addOrMergeImport(sourceFile, target, [symbol], false, sourceFile.getImportDeclarations().length); -} diff --git a/packages/codemod/src/utils/importUtils.ts b/packages/codemod/src/utils/importUtils.ts index 145c95328c..a1981cb14f 100644 --- a/packages/codemod/src/utils/importUtils.ts +++ b/packages/codemod/src/utils/importUtils.ts @@ -7,6 +7,7 @@ const V2_PACKAGES = new Set([ '@modelcontextprotocol/client', '@modelcontextprotocol/server', '@modelcontextprotocol/core', + '@modelcontextprotocol/sdk-shared', '@modelcontextprotocol/node', '@modelcontextprotocol/express' ]); diff --git a/packages/codemod/test/commentInsertion.test.ts b/packages/codemod/test/commentInsertion.test.ts index a50c4698be..2dca284968 100644 --- a/packages/codemod/test/commentInsertion.test.ts +++ b/packages/codemod/test/commentInsertion.test.ts @@ -87,11 +87,12 @@ describe('comment insertion', () => { it('inserts multiple comments in one file in correct positions', () => { const dir = createTempDir(); - // Two .parse() calls on different schemas trigger two actionRequired diagnostics + // Two custom-schema handler registrations on different lines trigger two actionRequired diagnostics const input = [ - `import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';`, - `const a = CallToolRequestSchema.parse(data1);`, - `const b = ListToolsRequestSchema.parse(data2);`, + `import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';`, + `const server = new McpServer({ name: 'test', version: '1.0' });`, + `server.setRequestHandler(FooSchema, async () => ({}));`, + `server.setRequestHandler(BarSchema, async () => ({}));`, `` ].join('\n'); writeFileSync(path.join(dir, 'server.ts'), input); @@ -107,9 +108,9 @@ describe('comment insertion', () => { it('preserves indentation of the target line', () => { const dir = createTempDir(); const input = [ - `import { CallToolRequestSchema } from '@modelcontextprotocol/sdk/types.js';`, - `function validate() {`, - ` const a = CallToolRequestSchema.parse(data);`, + `import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';`, + `function register(server: McpServer) {`, + ` server.setRequestHandler(FooSchema, async () => ({}));`, `}`, `` ].join('\n'); @@ -125,8 +126,9 @@ describe('comment insertion', () => { it('does not duplicate comments on re-run (idempotency)', () => { const dir = createTempDir(); const input = [ - `import { CallToolRequestSchema } from '@modelcontextprotocol/sdk/types.js';`, - `const a = CallToolRequestSchema.parse(data);`, + `import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';`, + `const server = new McpServer({ name: 'test', version: '1.0' });`, + `server.setRequestHandler(FooSchema, async () => ({}));`, `` ].join('\n'); writeFileSync(path.join(dir, 'server.ts'), input); @@ -146,10 +148,11 @@ describe('comment insertion', () => { it('sanitizes */ in diagnostic messages', () => { const dir = createTempDir(); - // The .parse() diagnostic message doesn't contain */, but we verify the comment is well-formed + // The handler diagnostic message doesn't contain */, but we verify the comment is well-formed const input = [ - `import { CallToolRequestSchema } from '@modelcontextprotocol/sdk/types.js';`, - `const a = CallToolRequestSchema.parse(data);`, + `import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';`, + `const server = new McpServer({ name: 'test', version: '1.0' });`, + `server.setRequestHandler(FooSchema, async () => ({}));`, `` ].join('\n'); writeFileSync(path.join(dir, 'server.ts'), input); @@ -167,10 +170,10 @@ describe('comment insertion', () => { // Import rewrite adds new import lines (splitting into multiple packages), // then handler transform emits actionRequired. The comment must land at the correct post-shift line. const input = [ - `import { McpServer, CallToolRequestSchema } from '@modelcontextprotocol/sdk/server/mcp.js';`, + `import { McpServer, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/server/mcp.js';`, ``, `const server = new McpServer({ name: 'test', version: '1.0' });`, - `const a = CallToolRequestSchema.parse(data);`, + `server.setRequestHandler(FooSchema, async () => ({}));`, `` ].join('\n'); writeFileSync(path.join(dir, 'server.ts'), input); @@ -181,16 +184,18 @@ describe('comment insertion', () => { const lines = output.split('\n'); const commentIdx = lines.findIndex(l => l.includes(CODEMOD_ERROR_PREFIX)); expect(commentIdx).toBeGreaterThan(-1); - // The comment should be directly above the parse() line (which may have moved) + // The comment should be directly above the handler line (which may have moved) const nextLine = lines[commentIdx + 1]!; - expect(nextLine).toContain('.parse(data)'); + expect(nextLine).toContain('setRequestHandler'); }); it('merges same-line diagnostics into a single comment', () => { const dir = createTempDir(); + // Two custom-schema handler registrations on the SAME physical line -> two same-line diagnostics const input = [ - `import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';`, - `const a = CallToolRequestSchema.parse(data1); const b = ListToolsRequestSchema.parse(data2);`, + `import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';`, + `const server = new McpServer({ name: 'test', version: '1.0' });`, + `server.setRequestHandler(FooSchema, async () => ({})); server.setRequestHandler(BarSchema, async () => ({}));`, `` ].join('\n'); writeFileSync(path.join(dir, 'server.ts'), input); @@ -207,9 +212,10 @@ describe('comment insertion', () => { it('skips comment insertion when target line is inside a template literal', () => { const dir = createTempDir(); const input = [ - "import { CallToolRequestSchema } from '@modelcontextprotocol/sdk/types.js';", + "import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';", + "const server = new McpServer({ name: 'test', version: '1.0' });", 'const msg = `', - ' Result: ${CallToolRequestSchema.parse(data).method}', + ' Result: ${server.setRequestHandler(FooSchema, async () => ({}))}', '`;', '' ].join('\n'); @@ -230,10 +236,11 @@ describe('comment insertion', () => { const dir = createTempDir(); // TemplateMiddle: text between two ${} spans const input = [ - "import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';", + "import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';", + "const server = new McpServer({ name: 'test', version: '1.0' });", 'const msg = `${somePrefix}', - ' A: ${CallToolRequestSchema.parse(d1)}', - ' B: ${ListToolsRequestSchema.parse(d2)}', + ' A: ${server.setRequestHandler(FooSchema, async () => ({}))}', + ' B: ${server.setRequestHandler(BarSchema, async () => ({}))}', '`;', '' ].join('\n'); @@ -249,11 +256,12 @@ describe('comment insertion', () => { it('still inserts comment when diagnostic line merely contains a template literal', () => { const dir = createTempDir(); - // The .parse() and template are on the same line, but lineStart is at "const", + // The handler call and template are on the same line, but lineStart is at "server", // which is outside the template literal. const input = [ - "import { CallToolRequestSchema } from '@modelcontextprotocol/sdk/types.js';", - 'const a = CallToolRequestSchema.parse(`template ${data}`);', + "import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';", + "const server = new McpServer({ name: 'test', version: '1.0' });", + 'server.setRequestHandler(FooSchema, async () => ({ msg: `template ${data}` }));', '' ].join('\n'); writeFileSync(path.join(dir, 'server.ts'), input); @@ -272,8 +280,9 @@ describe('comment insertion', () => { it('handles CRLF line endings without corrupting the file', () => { const dir = createTempDir(); const input = [ - `import { CallToolRequestSchema } from '@modelcontextprotocol/sdk/types.js';`, - `const a = CallToolRequestSchema.parse(data);`, + `import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';`, + `const server = new McpServer({ name: 'test', version: '1.0' });`, + `server.setRequestHandler(FooSchema, async () => ({}));`, `` ].join('\r\n'); writeFileSync(path.join(dir, 'server.ts'), input); @@ -286,6 +295,6 @@ describe('comment insertion', () => { const commentIdx = lines.findIndex(l => l.includes(CODEMOD_ERROR_PREFIX)); expect(commentIdx).toBeGreaterThan(-1); expect(lines[commentIdx]!.trim()).toMatch(/^\/\*.*\*\/$/); - expect(lines[commentIdx + 1]).toContain('.parse(data)'); + expect(lines[commentIdx + 1]).toContain('setRequestHandler'); }); }); diff --git a/packages/codemod/test/v1-to-v2/transforms/importPaths.test.ts b/packages/codemod/test/v1-to-v2/transforms/importPaths.test.ts index 8a63497cac..c0569973a3 100644 --- a/packages/codemod/test/v1-to-v2/transforms/importPaths.test.ts +++ b/packages/codemod/test/v1-to-v2/transforms/importPaths.test.ts @@ -74,25 +74,100 @@ describe('import-paths transform', () => { expect(result.diagnostics[0]!.message).toContain('SSEServerTransport is deprecated'); }); - it('resolves sdk/types.js based on sibling client imports', () => { + it('resolves a sdk/types.js TYPE import based on sibling client imports', () => { const input = [ `import { Client } from '@modelcontextprotocol/sdk/client/index.js';`, - `import { CallToolResultSchema } from '@modelcontextprotocol/sdk/types.js';`, + `import { CallToolResult } from '@modelcontextprotocol/sdk/types.js';`, '' ].join('\n'); const result = applyTransform(input, { projectType: 'both' }); expect(result).toContain(`from "@modelcontextprotocol/client"`); + expect(result).toContain('CallToolResult'); + expect(result).not.toContain('@modelcontextprotocol/sdk-shared'); + }); + + it('resolves a sdk/types.js TYPE import based on sibling server imports', () => { + const input = [ + `import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';`, + `import { CallToolResult } from '@modelcontextprotocol/sdk/types.js';`, + '' + ].join('\n'); + const result = applyTransform(input, { projectType: 'both' }); + expect(result).toContain(`from "@modelcontextprotocol/server"`); + expect(result).toContain('CallToolResult'); + }); + + it('routes *Schema imports from sdk/types.js to @modelcontextprotocol/sdk-shared', () => { + const input = `import { CallToolResultSchema } from '@modelcontextprotocol/sdk/types.js';\n`; + const result = applyTransform(input, { projectType: 'server' }); + expect(result).toContain(`from "@modelcontextprotocol/sdk-shared"`); expect(result).toContain('CallToolResultSchema'); + expect(result).not.toContain('@modelcontextprotocol/sdk/types'); }); - it('resolves sdk/types.js based on sibling server imports', () => { + it('routes schemas to sdk-shared regardless of client/server sibling context', () => { + // The only sibling is a client import, but the schema must still go to sdk-shared. + const input = [ + `import { Client } from '@modelcontextprotocol/sdk/client/index.js';`, + `import { ListToolsResultSchema } from '@modelcontextprotocol/sdk/types.js';`, + '' + ].join('\n'); + const result = applyTransform(input, { projectType: 'both' }); + expect(result).toContain(`from "@modelcontextprotocol/sdk-shared"`); + expect(result).toContain('ListToolsResultSchema'); + }); + + it('splits a mixed type + schema import: type resolves by context, schema to sdk-shared', () => { const input = [ `import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';`, - `import { CallToolResultSchema } from '@modelcontextprotocol/sdk/types.js';`, + `import { CallToolResult, CallToolResultSchema } from '@modelcontextprotocol/sdk/types.js';`, '' ].join('\n'); const result = applyTransform(input, { projectType: 'both' }); + expect(result).toContain(`from "@modelcontextprotocol/sdk-shared"`); expect(result).toContain(`from "@modelcontextprotocol/server"`); + expect(result).toContain('CallToolResult'); + expect(result).toContain('CallToolResultSchema'); + expect(result).not.toContain('@modelcontextprotocol/sdk/types'); + }); + + it('does not rewrite schema .parse() usages (migrates as an import-path swap)', () => { + const input = [ + `import { CallToolResultSchema } from '@modelcontextprotocol/sdk/types.js';`, + `const r = CallToolResultSchema.parse(value);`, + '' + ].join('\n'); + const result = applyTransform(input, { projectType: 'server' }); + expect(result).toContain('CallToolResultSchema.parse(value)'); + expect(result).toContain(`from "@modelcontextprotocol/sdk-shared"`); + }); + + it('flags *Schema accesses through a namespace import of sdk/types.js (cannot be split)', () => { + const input = [ + `import * as types from '@modelcontextprotocol/sdk/types.js';`, + `const r = types.CallToolResultSchema.parse(value);`, + '' + ].join('\n'); + const project = new Project({ useInMemoryFileSystem: true }); + const sourceFile = project.createSourceFile('test.ts', input); + const result = importPathsTransform.apply(sourceFile, { projectType: 'server' }); + const messages = result.diagnostics.map(d => d.message).join('\n'); + // The namespace can't be split, so the schema can't be auto-routed — but the user must be told. + expect(messages).toContain('@modelcontextprotocol/sdk-shared'); + expect(messages).toContain('CallToolResultSchema'); + // The namespace import itself still moves to the context package (its types live there). + // (setModuleSpecifier preserves the original quote style, so match quote-agnostically.) + expect(sourceFile.getFullText()).toContain('@modelcontextprotocol/server'); + }); + + it('does not flag a namespace import of sdk/types.js that only accesses types', () => { + const input = [`import * as types from '@modelcontextprotocol/sdk/types.js';`, `const t: types.CallToolResult = value;`, ''].join( + '\n' + ); + const project = new Project({ useInMemoryFileSystem: true }); + const sourceFile = project.createSourceFile('test.ts', input); + const result = importPathsTransform.apply(sourceFile, { projectType: 'server' }); + expect(result.diagnostics.map(d => d.message).join('\n')).not.toContain('@modelcontextprotocol/sdk-shared'); }); it('resolves extensionless sdk/types (no .js suffix) the same as sdk/types.js', () => { diff --git a/packages/codemod/test/v1-to-v2/transforms/specSchemaAccess.test.ts b/packages/codemod/test/v1-to-v2/transforms/specSchemaAccess.test.ts deleted file mode 100644 index 2c7f592e1f..0000000000 --- a/packages/codemod/test/v1-to-v2/transforms/specSchemaAccess.test.ts +++ /dev/null @@ -1,517 +0,0 @@ -import { describe, it, expect } from 'vitest'; -import { Project } from 'ts-morph'; - -import { specSchemaAccessTransform } from '../../../src/migrations/v1-to-v2/transforms/specSchemaAccess.js'; -import type { TransformContext } from '../../../src/types.js'; - -const ctx: TransformContext = { projectType: 'server' }; - -function applyTransform(code: string) { - const project = new Project({ useInMemoryFileSystem: true }); - const sourceFile = project.createSourceFile('test.ts', code); - const result = specSchemaAccessTransform.apply(sourceFile, ctx); - return { text: sourceFile.getFullText(), result }; -} - -describe('spec-schema-access transform', () => { - describe('auto-transform: .safeParse(v).success → isSpecType.X(v)', () => { - it('rewrites XSchema.safeParse(v).success to isSpecType.X(v)', () => { - const input = [ - `import { CallToolRequestSchema } from '@modelcontextprotocol/server';`, - `const valid = CallToolRequestSchema.safeParse(data).success;`, - '' - ].join('\n'); - const { text, result } = applyTransform(input); - expect(text).toContain('isSpecType.CallToolRequest(data)'); - expect(text).not.toContain('safeParse'); - expect(result.changesCount).toBeGreaterThan(0); - }); - - it('handles safeParse().success in if-condition', () => { - const input = [ - `import { ToolSchema } from '@modelcontextprotocol/server';`, - `if (ToolSchema.safeParse(obj).success) { doSomething(); }`, - '' - ].join('\n'); - const { text } = applyTransform(input); - expect(text).toContain('isSpecType.Tool(obj)'); - expect(text).not.toContain('safeParse'); - }); - - it('adds isSpecType import when transforming safeParse().success', () => { - const input = [ - `import { CallToolResultSchema } from '@modelcontextprotocol/server';`, - `const ok = CallToolResultSchema.safeParse(x).success;`, - '' - ].join('\n'); - const { text } = applyTransform(input); - expect(text).toContain('isSpecType'); - expect(text).toMatch(/import.*isSpecType.*from/); - }); - }); - - describe('auto-transform: value position → specTypeSchemas.X', () => { - it('replaces schema passed as function arg with specTypeSchemas.X', () => { - const input = [ - `import { ListToolsRequestSchema } from '@modelcontextprotocol/server';`, - `validate(ListToolsRequestSchema);`, - '' - ].join('\n'); - const { text, result } = applyTransform(input); - expect(text).toContain('specTypeSchemas.ListToolsRequest'); - expect(result.changesCount).toBeGreaterThan(0); - expect(result.diagnostics.length).toBeGreaterThan(0); - expect(result.diagnostics[0]!.message).toContain('StandardSchemaV1'); - }); - - it('adds specTypeSchemas import', () => { - const input = [`import { ToolSchema } from '@modelcontextprotocol/server';`, `const s = ToolSchema;`, ''].join('\n'); - const { text } = applyTransform(input); - expect(text).toContain('specTypeSchemas.Tool'); - expect(text).toMatch(/import.*specTypeSchemas.*from/); - }); - }); - - describe('auto-transform: captured safeParse result', () => { - it('rewrites captured safeParse call and result property accesses', () => { - const input = [ - `import { CallToolResultSchema } from '@modelcontextprotocol/server';`, - `const parsed = CallToolResultSchema.safeParse(data);`, - `if (parsed.success) { return parsed.data; }`, - '' - ].join('\n'); - const { text, result } = applyTransform(input); - expect(text).toContain("specTypeSchemas.CallToolResult['~standard'].validate(data)"); - expect(text).toContain('parsed.issues === undefined'); - expect(text).toContain('parsed.value'); - expect(text).not.toContain('safeParse'); - expect(text).not.toContain('parsed.success'); - expect(text).not.toContain('parsed.data'); - expect(result.changesCount).toBeGreaterThan(0); - }); - - it('rewrites result properties assigned to variables (const isValid = parsed.success)', () => { - const input = [ - `import { CallToolResultSchema } from '@modelcontextprotocol/server';`, - `const parsed = CallToolResultSchema.safeParse(data);`, - `const isValid = parsed.success;`, - `const result = parsed.data;`, - '' - ].join('\n'); - const { text } = applyTransform(input); - expect(text).toContain('parsed.issues === undefined'); - expect(text).toContain('parsed.value'); - expect(text).not.toContain('parsed.success'); - expect(text).not.toContain('parsed.data'); - }); - - it('rewrites .error to .issues', () => { - const input = [ - `import { ToolSchema } from '@modelcontextprotocol/server';`, - `const result = ToolSchema.safeParse(raw);`, - `if (!result.success) { console.log(result.error); }`, - '' - ].join('\n'); - const { text } = applyTransform(input); - expect(text).toContain('result.issues'); - expect(text).not.toContain('result.error'); - }); - - it('handles ternary pattern: x.success ? x.data : fallback', () => { - const input = [ - `import { CallToolResultSchema } from '@modelcontextprotocol/server';`, - `const parsed = CallToolResultSchema.safeParse(toolResult);`, - `return parsed.success ? parsed.data : undefined;`, - '' - ].join('\n'); - const { text } = applyTransform(input); - expect(text).toContain("specTypeSchemas.CallToolResult['~standard'].validate(toolResult)"); - expect(text).toContain('(parsed.issues === undefined) ? parsed.value : undefined'); - }); - - it('adds specTypeSchemas import', () => { - const input = [ - `import { ToolSchema } from '@modelcontextprotocol/server';`, - `const r = ToolSchema.safeParse(v);`, - `r.success;`, - '' - ].join('\n'); - const { text } = applyTransform(input); - expect(text).toMatch(/import.*specTypeSchemas.*from/); - }); - - it('rewrites .error.issues to .issues (unwrap double nesting)', () => { - const input = [ - `import { CallToolResultSchema } from '@modelcontextprotocol/server';`, - `const parsed = CallToolResultSchema.safeParse(data);`, - `if (!parsed.success) { console.log(parsed.error.issues); }`, - '' - ].join('\n'); - const { text } = applyTransform(input); - expect(text).toContain('parsed.issues'); - expect(text).not.toContain('parsed.issues.issues'); - expect(text).not.toContain('parsed.error'); - }); - - it('rewrites .error.message to issues map expression', () => { - const input = [ - `import { CallToolResultSchema } from '@modelcontextprotocol/server';`, - `const parsed = CallToolResultSchema.safeParse(data);`, - `if (!parsed.success) { console.log(parsed.error.message); }`, - '' - ].join('\n'); - const { text } = applyTransform(input); - expect(text).not.toContain('parsed.error'); - expect(text).not.toContain('parsed.issues.message'); - expect(text).toContain("parsed.issues?.map(i => i.message).join(', ')"); - }); - - it('emits diagnostic for .error.format() instead of silently rewriting', () => { - const input = [ - `import { CallToolResultSchema } from '@modelcontextprotocol/server';`, - `const parsed = CallToolResultSchema.safeParse(data);`, - `if (!parsed.success) { console.log(parsed.error.format()); }`, - '' - ].join('\n'); - const { text, result } = applyTransform(input); - expect(text).toContain('parsed.error.format()'); - expect(text).not.toContain('parsed.issues()'); - expect(result.diagnostics.some(d => d.message.includes('no StandardSchema equivalent'))).toBe(true); - }); - - it('rewrites bare .error to .issues (unchanged behavior)', () => { - const input = [ - `import { ToolSchema } from '@modelcontextprotocol/server';`, - `const result = ToolSchema.safeParse(raw);`, - `if (!result.success) { console.log(result.error); }`, - '' - ].join('\n'); - const { text } = applyTransform(input); - expect(text).toContain('result.issues'); - expect(text).not.toContain('result.error'); - }); - - it('does not rewrite same-named variable in sibling function', () => { - const input = [ - `import { CallToolRequestSchema } from '@modelcontextprotocol/sdk/types.js';`, - `function validate(d: unknown) {`, - ` const result = CallToolRequestSchema.safeParse(d);`, - ` return result.success;`, - `}`, - `async function callApi(client: any) {`, - ` const result = await client.get('/api');`, - ` return result.data;`, - `}`, - '' - ].join('\n'); - const { text } = applyTransform(input); - expect(text).toContain('result.issues === undefined'); - expect(text).toContain('return result.data'); - expect(text).not.toContain('return result.value'); - }); - - it('rewrites non-captured safeParse (bare expression) to validate()', () => { - const input = [`import { ToolSchema } from '@modelcontextprotocol/server';`, `ToolSchema.safeParse(data);`, ''].join('\n'); - const { text, result } = applyTransform(input); - expect(text).toContain("specTypeSchemas.Tool['~standard'].validate(data)"); - expect(text).not.toMatch(/import\s*\{[^}]*ToolSchema[^}]*\}/); - expect(result.changesCount).toBeGreaterThan(0); - expect(result.diagnostics.length).toBe(1); - }); - }); - - describe('guardrails: non-MCP schemas are NOT touched', () => { - it('does not rewrite safeParse on user-defined schema with same name from local import', () => { - const input = [ - `import { CallToolResultSchema } from './mySchemas';`, - `const parsed = CallToolResultSchema.safeParse(data);`, - `if (parsed.success) { return parsed.data; }`, - '' - ].join('\n'); - const { text, result } = applyTransform(input); - expect(text).toContain('CallToolResultSchema.safeParse'); - expect(text).toContain('parsed.success'); - expect(text).toContain('parsed.data'); - expect(result.changesCount).toBe(0); - expect(result.diagnostics.length).toBe(0); - }); - - it('does not rewrite safeParse on user zod schema not from MCP', () => { - const input = [ - `import { z } from 'zod';`, - `const MySchema = z.object({ name: z.string() });`, - `const parsed = MySchema.safeParse(data);`, - `if (parsed.success) { return parsed.data; }`, - '' - ].join('\n'); - const { text, result } = applyTransform(input); - expect(text).toContain('MySchema.safeParse'); - expect(text).toContain('parsed.success'); - expect(text).toContain('parsed.data'); - expect(result.changesCount).toBe(0); - expect(result.diagnostics.length).toBe(0); - }); - - it('does not rewrite safeParse on non-spec schema name from MCP import', () => { - const input = [ - `import { SomeRandomSchema } from '@modelcontextprotocol/server';`, - `const parsed = SomeRandomSchema.safeParse(data);`, - `if (parsed.success) { return parsed.data; }`, - '' - ].join('\n'); - const { text, result } = applyTransform(input); - expect(text).toContain('SomeRandomSchema.safeParse'); - expect(text).toContain('parsed.success'); - expect(result.changesCount).toBe(0); - expect(result.diagnostics.length).toBe(0); - }); - - it('does not rewrite safeParse on npm package schema with matching name', () => { - const input = [ - `import { CallToolResultSchema } from 'some-other-package';`, - `const parsed = CallToolResultSchema.safeParse(data);`, - `if (parsed.success) { return parsed.data; }`, - '' - ].join('\n'); - const { text, result } = applyTransform(input); - expect(text).toContain('CallToolResultSchema.safeParse'); - expect(text).toContain('parsed.success'); - expect(result.changesCount).toBe(0); - expect(result.diagnostics.length).toBe(0); - }); - }); - - describe('auto-transform: generic property access → specTypeSchemas.X', () => { - it('replaces schema identifier in .parseAsync() call', () => { - const input = [ - `import { OAuthTokensSchema } from '@modelcontextprotocol/server';`, - `const tokens = await OAuthTokensSchema.parseAsync(data);`, - '' - ].join('\n'); - const { text, result } = applyTransform(input); - expect(text).toContain('specTypeSchemas.OAuthTokens.parseAsync(data)'); - expect(text).not.toMatch(/import\s*\{[^}]*OAuthTokensSchema[^}]*\}/); - expect(result.changesCount).toBeGreaterThan(0); - expect(result.diagnostics.length).toBeGreaterThan(0); - }); - - it('replaces schema identifier in .or() call', () => { - const input = [ - `import { ServerNotificationSchema } from '@modelcontextprotocol/server';`, - `const union = ServerNotificationSchema.or(otherSchema);`, - '' - ].join('\n'); - const { text, result } = applyTransform(input); - expect(text).toContain('specTypeSchemas.ServerNotification.or(otherSchema)'); - expect(text).not.toMatch(/import\s*\{[^}]*ServerNotificationSchema[^}]*\}/); - expect(result.changesCount).toBeGreaterThan(0); - }); - - it('replaces schema identifier in .extend() call', () => { - const input = [ - `import { ToolSchema } from '@modelcontextprotocol/server';`, - `const extended = ToolSchema.extend({ extra: z.string() });`, - '' - ].join('\n'); - const { text, result } = applyTransform(input); - expect(text).toContain('specTypeSchemas.Tool.extend'); - expect(result.changesCount).toBeGreaterThan(0); - }); - - it('adds specTypeSchemas import for generic property access', () => { - const input = [ - `import { OAuthTokensSchema } from '@modelcontextprotocol/server';`, - `const tokens = await OAuthTokensSchema.parseAsync(data);`, - '' - ].join('\n'); - const { text } = applyTransform(input); - expect(text).toMatch(/import.*specTypeSchemas.*from/); - }); - }); - - describe('.parse(v)', () => { - it('rewrites discarded parse() to the validate() primitive', () => { - const input = [`import { ToolSchema } from '@modelcontextprotocol/server';`, `ToolSchema.parse(raw);`, ''].join('\n'); - const { text, result } = applyTransform(input); - expect(text).toContain("specTypeSchemas.Tool['~standard'].validate(raw)"); - expect(text).not.toMatch(/import\s*\{[^}]*ToolSchema[^}]*\}/); - expect(result.changesCount).toBeGreaterThan(0); - }); - - it('swaps the identifier (import stays resolvable) when the parse() result is used', () => { - const input = [`import { ToolSchema } from '@modelcontextprotocol/server';`, `const tool = ToolSchema.parse(raw);`, ''].join( - '\n' - ); - const { text, result } = applyTransform(input); - expect(text).toContain('specTypeSchemas.Tool.parse(raw)'); - expect(text).not.toMatch(/import\s*\{[^}]*ToolSchema[^}]*\}/); - expect(result.changesCount).toBeGreaterThan(0); - expect(result.diagnostics[0]!.message).toContain('specTypeSchemas.Tool'); - }); - }); - - describe('diagnostic: z.infer', () => { - it('emits diagnostic for typeof in type position', () => { - const input = [ - `import { CallToolResultSchema } from '@modelcontextprotocol/client';`, - `type Result = typeof CallToolResultSchema;`, - '' - ].join('\n'); - const { result } = applyTransform(input); - expect(result.diagnostics.length).toBe(1); - expect(result.diagnostics[0]!.message).toContain('CallToolResult'); - }); - }); - - describe('no-op cases', () => { - it('does nothing for non-MCP imports', () => { - const input = [`import { CallToolRequestSchema } from './local';`, `CallToolRequestSchema.safeParse(data);`, ''].join('\n'); - const { text, result } = applyTransform(input); - expect(text).toContain('CallToolRequestSchema.safeParse'); - expect(result.changesCount).toBe(0); - expect(result.diagnostics.length).toBe(0); - }); - - it('does nothing for non-spec schema names', () => { - const input = [`import { SomeRandomSchema } from '@modelcontextprotocol/server';`, `SomeRandomSchema.parse(data);`, ''].join( - '\n' - ); - const { text, result } = applyTransform(input); - expect(text).toContain('SomeRandomSchema.parse'); - expect(result.changesCount).toBe(0); - expect(result.diagnostics.length).toBe(0); - }); - - it('does nothing when no remaining references', () => { - const input = [`import { CallToolRequestSchema } from '@modelcontextprotocol/server';`, ''].join('\n'); - const { result } = applyTransform(input); - expect(result.changesCount).toBe(0); - expect(result.diagnostics.length).toBe(0); - }); - }); - - describe('import cleanup after transform', () => { - it('removes original schema import after all refs are auto-transformed', () => { - const input = [ - `import { CallToolRequestSchema } from '@modelcontextprotocol/server';`, - `const valid = CallToolRequestSchema.safeParse(data).success;`, - '' - ].join('\n'); - const { text } = applyTransform(input); - expect(text).toContain('isSpecType.CallToolRequest(data)'); - expect(text).not.toMatch(/import\s*\{[^}]*CallToolRequestSchema[^}]*\}/); - }); - - it('removes the schema import even when a ref falls back to a parse()/safeParse() rewrite', () => { - const input = [ - `import { CallToolRequestSchema } from '@modelcontextprotocol/server';`, - `const valid = CallToolRequestSchema.safeParse(data).success;`, - `const parsed = CallToolRequestSchema.parse(data);`, - '' - ].join('\n'); - const { text } = applyTransform(input); - expect(text).toContain('isSpecType.CallToolRequest(data)'); - expect(text).toContain('specTypeSchemas.CallToolRequest.parse(data)'); - expect(text).not.toMatch(/import\s*\{[^}]*CallToolRequestSchema[^}]*\}/); - }); - - it('removes schema specifier from import that also has other symbols', () => { - const input = [ - `import { CallToolRequestSchema, McpError } from '@modelcontextprotocol/server';`, - `const valid = CallToolRequestSchema.safeParse(data).success;`, - `throw new McpError(1, 'fail');`, - '' - ].join('\n'); - const { text } = applyTransform(input); - expect(text).not.toMatch(/import\s*\{[^}]*CallToolRequestSchema[^}]*\}/); - expect(text).toContain('McpError'); - expect(text).toContain(`@modelcontextprotocol/server`); - }); - }); - - describe('parent-kind guards', () => { - it('emits diagnostic for re-exported schema (ExportSpecifier)', () => { - const input = [ - `import { CallToolRequestSchema } from '@modelcontextprotocol/server';`, - `export { CallToolRequestSchema };`, - '' - ].join('\n'); - const { text, result } = applyTransform(input); - expect(text).toContain('export { CallToolRequestSchema }'); - expect(result.diagnostics.some(d => d.message.includes('Re-export'))).toBe(true); - expect(result.changesCount).toBe(0); - }); - - it('expands shorthand property assignment and removes import', () => { - const input = [`import { ToolSchema } from '@modelcontextprotocol/server';`, `const schemas = { ToolSchema };`, ''].join('\n'); - const { text, result } = applyTransform(input); - expect(text).toContain("'ToolSchema': specTypeSchemas.Tool"); - expect(text).not.toMatch(/import\s*\{[^}]*ToolSchema[^}]*\}/); - expect(result.changesCount).toBeGreaterThan(0); - }); - - it('skips PropertyAssignment name-node (non-shorthand)', () => { - const input = [ - `import { ToolSchema } from '@modelcontextprotocol/server';`, - `const schemas = { ToolSchema: myValidator };`, - '' - ].join('\n'); - const { text, result } = applyTransform(input); - expect(text).toContain('ToolSchema: myValidator'); - expect(result.changesCount).toBe(0); - }); - - it('skips BindingElement property-name', () => { - const input = [`import { ToolSchema } from '@modelcontextprotocol/server';`, `const { ToolSchema: local } = obj;`, ''].join( - '\n' - ); - const { text, result } = applyTransform(input); - expect(text).toContain('ToolSchema: local'); - expect(result.changesCount).toBe(0); - }); - - it('skips PropertyAccessExpression name-node (obj.ToolSchema)', () => { - const input = [`import { ToolSchema } from '@modelcontextprotocol/server';`, `const x = registry.ToolSchema;`, ''].join('\n'); - const { text, result } = applyTransform(input); - expect(text).toContain('registry.ToolSchema'); - expect(text).not.toContain('specTypeSchemas'); - expect(result.changesCount).toBe(0); - }); - - it('does not emit z.infer diagnostic for runtime typeof (TypeOfExpression)', () => { - const input = [`import { ToolSchema } from '@modelcontextprotocol/server';`, `const kind = typeof ToolSchema;`, ''].join('\n'); - const { result } = applyTransform(input); - expect(result.diagnostics.every(d => !d.message.includes('z.infer'))).toBe(true); - }); - }); - - describe('namespace imports', () => { - it('does not crash when file has namespace import from same package', () => { - const input = [ - `import * as types from '@modelcontextprotocol/server';`, - `import { ToolSchema } from '@modelcontextprotocol/server';`, - `const s = ToolSchema;`, - '' - ].join('\n'); - const { text, result } = applyTransform(input); - expect(text).toContain('specTypeSchemas.Tool'); - expect(result.changesCount).toBeGreaterThan(0); - }); - }); - - describe('aliased imports', () => { - it('handles aliased import and auto-transforms captured safeParse', () => { - const input = [ - `import { CallToolRequestSchema as CTRS } from '@modelcontextprotocol/server';`, - `const result = CTRS.safeParse(data);`, - `result.success;`, - '' - ].join('\n'); - const { text, result } = applyTransform(input); - expect(text).toContain("specTypeSchemas.CallToolRequest['~standard'].validate(data)"); - expect(text).not.toContain('CTRS.safeParse'); - expect(result.changesCount).toBeGreaterThan(0); - expect(result.diagnostics[0]!.message).toContain('specTypeSchemas.CallToolRequest'); - }); - }); -}); diff --git a/packages/sdk-shared/README.md b/packages/sdk-shared/README.md new file mode 100644 index 0000000000..93ecf61690 --- /dev/null +++ b/packages/sdk-shared/README.md @@ -0,0 +1,34 @@ +# @modelcontextprotocol/sdk-shared + +Canonical public home for the [Model Context Protocol](https://modelcontextprotocol.io) specification **Zod schemas**. + +These are the exact schema constants the SDK validates protocol payloads against internally. The `@modelcontextprotocol/server` and `@modelcontextprotocol/client` packages keep a Zod-free public surface, so this package exists as the supported place to import the raw schemas when +you need to validate or parse MCP messages yourself. + +## Install + +```sh +npm install @modelcontextprotocol/sdk-shared +``` + +## Usage + +```ts +import { CallToolResultSchema } from '@modelcontextprotocol/sdk-shared'; + +// Throws on invalid input; returns the typed result on success. +const result = CallToolResultSchema.parse(payload); + +// Or non-throwing: +const parsed = CallToolResultSchema.safeParse(payload); +if (parsed.success) { + // parsed.data is a fully typed CallToolResult +} +``` + +## Scope + +This package exports **only** the spec Zod schemas (`*Schema`). The corresponding TypeScript types, error classes, enums, and type guards are part of the public API of [`@modelcontextprotocol/server`](https://www.npmjs.com/package/@modelcontextprotocol/server) and +[`@modelcontextprotocol/client`](https://www.npmjs.com/package/@modelcontextprotocol/client). + +> **Migrating from v1?** In v1 these schemas were imported from `@modelcontextprotocol/sdk/types.js`. Point those `*Schema` imports at `@modelcontextprotocol/sdk-shared` and your existing `.parse()` / `.safeParse()` calls keep working unchanged. diff --git a/packages/sdk-shared/eslint.config.mjs b/packages/sdk-shared/eslint.config.mjs new file mode 100644 index 0000000000..4f034f2235 --- /dev/null +++ b/packages/sdk-shared/eslint.config.mjs @@ -0,0 +1,12 @@ +// @ts-check + +import baseConfig from '@modelcontextprotocol/eslint-config'; + +export default [ + ...baseConfig, + { + settings: { + 'import/internal-regex': '^@modelcontextprotocol/core' + } + } +]; diff --git a/packages/sdk-shared/package.json b/packages/sdk-shared/package.json new file mode 100644 index 0000000000..4e54407c72 --- /dev/null +++ b/packages/sdk-shared/package.json @@ -0,0 +1,63 @@ +{ + "name": "@modelcontextprotocol/sdk-shared", + "version": "2.0.0-alpha.0", + "description": "Model Context Protocol implementation for TypeScript - shared spec Zod schemas", + "license": "MIT", + "author": "Anthropic, PBC (https://anthropic.com)", + "homepage": "https://modelcontextprotocol.io", + "bugs": "https://github.com/modelcontextprotocol/typescript-sdk/issues", + "type": "module", + "repository": { + "type": "git", + "url": "git+https://github.com/modelcontextprotocol/typescript-sdk.git" + }, + "engines": { + "node": ">=20" + }, + "keywords": [ + "modelcontextprotocol", + "mcp", + "schemas", + "zod" + ], + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + } + }, + "types": "./dist/index.d.ts", + "files": [ + "dist" + ], + "scripts": { + "typecheck": "tsgo -p tsconfig.json --noEmit", + "build": "tsdown", + "build:watch": "tsdown --watch", + "prepack": "pnpm run build", + "lint": "eslint src/ && prettier --ignore-path ../../.prettierignore --check .", + "lint:fix": "eslint src/ --fix && prettier --ignore-path ../../.prettierignore --write .", + "check": "pnpm run typecheck && pnpm run lint", + "test": "vitest run", + "test:watch": "vitest" + }, + "dependencies": { + "zod": "catalog:runtimeShared" + }, + "devDependencies": { + "@eslint/js": "catalog:devTools", + "@modelcontextprotocol/core": "workspace:^", + "@modelcontextprotocol/eslint-config": "workspace:^", + "@modelcontextprotocol/tsconfig": "workspace:^", + "@modelcontextprotocol/vitest-config": "workspace:^", + "@typescript/native-preview": "catalog:devTools", + "eslint": "catalog:devTools", + "eslint-config-prettier": "catalog:devTools", + "eslint-plugin-n": "catalog:devTools", + "prettier": "catalog:devTools", + "tsdown": "catalog:devTools", + "typescript": "catalog:devTools", + "typescript-eslint": "catalog:devTools", + "vitest": "catalog:devTools" + } +} diff --git a/packages/sdk-shared/src/index.ts b/packages/sdk-shared/src/index.ts new file mode 100644 index 0000000000..89ac033328 --- /dev/null +++ b/packages/sdk-shared/src/index.ts @@ -0,0 +1,177 @@ +// @modelcontextprotocol/sdk-shared +// +// Canonical public home for the Model Context Protocol specification Zod schemas. +// +// These are the exact schema constants the SDK validates against internally (defined in the +// private @modelcontextprotocol/core package). This package bundles core and re-exports ONLY the +// spec `*Schema` Zod values, so consumers can validate protocol payloads directly — e.g. +// `CallToolResultSchema.parse(value)` / `.safeParse(value)` — without depending on core's +// internal barrel. +// +// Scope: Zod schemas ONLY. The corresponding spec TypeScript types, error classes, enums, and +// type guards are part of the public API of @modelcontextprotocol/server and /client. +// +// The list below is the complete set of `export const *Schema` declarations in core's schema +// module; the sdkSharedSchemas test asserts it stays in sync. The @modelcontextprotocol/core +// specifier is aliased (tsconfig.json + tsdown.config.ts) to core's schemas module and bundled. +export { + AnnotationsSchema, + AudioContentSchema, + BaseMetadataSchema, + BaseRequestParamsSchema, + BlobResourceContentsSchema, + BooleanSchemaSchema, + CallToolRequestParamsSchema, + CallToolRequestSchema, + CallToolResultSchema, + CancelledNotificationParamsSchema, + CancelledNotificationSchema, + CancelTaskRequestSchema, + CancelTaskResultSchema, + ClientCapabilitiesSchema, + ClientNotificationSchema, + ClientRequestSchema, + ClientResultSchema, + ClientTasksCapabilitySchema, + CompatibilityCallToolResultSchema, + CompleteRequestParamsSchema, + CompleteRequestSchema, + CompleteResultSchema, + ContentBlockSchema, + CreateMessageRequestParamsSchema, + CreateMessageRequestSchema, + CreateMessageResultSchema, + CreateMessageResultWithToolsSchema, + CreateTaskResultSchema, + CursorSchema, + DiscoverRequestSchema, + DiscoverResultSchema, + ElicitationCompleteNotificationParamsSchema, + ElicitationCompleteNotificationSchema, + ElicitRequestFormParamsSchema, + ElicitRequestParamsSchema, + ElicitRequestSchema, + ElicitRequestURLParamsSchema, + ElicitResultSchema, + EmbeddedResourceSchema, + EmptyResultSchema, + EnumSchemaSchema, + GetPromptRequestParamsSchema, + GetPromptRequestSchema, + GetPromptResultSchema, + GetTaskPayloadRequestSchema, + GetTaskPayloadResultSchema, + GetTaskRequestSchema, + GetTaskResultSchema, + IconSchema, + IconsSchema, + ImageContentSchema, + ImplementationSchema, + InitializedNotificationSchema, + InitializeRequestParamsSchema, + InitializeRequestSchema, + InitializeResultSchema, + JSONArraySchema, + JSONObjectSchema, + JSONRPCErrorResponseSchema, + JSONRPCMessageSchema, + JSONRPCNotificationSchema, + JSONRPCRequestSchema, + JSONRPCResponseSchema, + JSONRPCResultResponseSchema, + JSONValueSchema, + LegacyTitledEnumSchemaSchema, + ListChangedOptionsBaseSchema, + ListPromptsRequestSchema, + ListPromptsResultSchema, + ListResourcesRequestSchema, + ListResourcesResultSchema, + ListResourceTemplatesRequestSchema, + ListResourceTemplatesResultSchema, + ListRootsRequestSchema, + ListRootsResultSchema, + ListTasksRequestSchema, + ListTasksResultSchema, + ListToolsRequestSchema, + ListToolsResultSchema, + LoggingLevelSchema, + LoggingMessageNotificationParamsSchema, + LoggingMessageNotificationSchema, + ModelHintSchema, + ModelPreferencesSchema, + MultiSelectEnumSchemaSchema, + NotificationSchema, + NotificationsParamsSchema, + NumberSchemaSchema, + PaginatedRequestParamsSchema, + PaginatedRequestSchema, + PaginatedResultSchema, + PingRequestSchema, + PrimitiveSchemaDefinitionSchema, + ProgressNotificationParamsSchema, + ProgressNotificationSchema, + ProgressSchema, + ProgressTokenSchema, + PromptArgumentSchema, + PromptListChangedNotificationSchema, + PromptMessageSchema, + PromptReferenceSchema, + PromptSchema, + ReadResourceRequestParamsSchema, + ReadResourceRequestSchema, + ReadResourceResultSchema, + RelatedTaskMetadataSchema, + RequestIdSchema, + RequestMetaEnvelopeSchema, + RequestMetaSchema, + RequestSchema, + ResourceContentsSchema, + ResourceLinkSchema, + ResourceListChangedNotificationSchema, + ResourceRequestParamsSchema, + ResourceSchema, + ResourceTemplateReferenceSchema, + ResourceTemplateSchema, + ResourceUpdatedNotificationParamsSchema, + ResourceUpdatedNotificationSchema, + ResultSchema, + RoleSchema, + RootSchema, + RootsListChangedNotificationSchema, + SamplingContentSchema, + SamplingMessageContentBlockSchema, + SamplingMessageSchema, + ServerCapabilitiesSchema, + ServerNotificationSchema, + ServerRequestSchema, + ServerResultSchema, + ServerTasksCapabilitySchema, + SetLevelRequestParamsSchema, + SetLevelRequestSchema, + SingleSelectEnumSchemaSchema, + StringSchemaSchema, + SubscribeRequestParamsSchema, + SubscribeRequestSchema, + TaskAugmentedRequestParamsSchema, + TaskCreationParamsSchema, + TaskMetadataSchema, + TaskSchema, + TaskStatusNotificationParamsSchema, + TaskStatusNotificationSchema, + TaskStatusSchema, + TextContentSchema, + TextResourceContentsSchema, + TitledMultiSelectEnumSchemaSchema, + TitledSingleSelectEnumSchemaSchema, + ToolAnnotationsSchema, + ToolChoiceSchema, + ToolExecutionSchema, + ToolListChangedNotificationSchema, + ToolResultContentSchema, + ToolSchema, + ToolUseContentSchema, + UnsubscribeRequestParamsSchema, + UnsubscribeRequestSchema, + UntitledMultiSelectEnumSchemaSchema, + UntitledSingleSelectEnumSchemaSchema +} from '@modelcontextprotocol/core'; diff --git a/packages/sdk-shared/test/sdkSharedSchemas.test.ts b/packages/sdk-shared/test/sdkSharedSchemas.test.ts new file mode 100644 index 0000000000..aba0852f7c --- /dev/null +++ b/packages/sdk-shared/test/sdkSharedSchemas.test.ts @@ -0,0 +1,28 @@ +import { readFileSync } from 'node:fs'; +import { fileURLToPath } from 'node:url'; + +import { describe, expect, it } from 'vitest'; + +import * as sdkShared from '../src/index.js'; +import { CursorSchema, InitializeRequestSchema } from '../src/index.js'; + +describe('@modelcontextprotocol/sdk-shared', () => { + it('re-exports spec schemas as working Zod objects', () => { + // Round-trips a valid value and rejects an invalid one — proves the re-exports are the + // real Zod schemas (not type-only aliases) and that `.parse`/`.safeParse` work. + expect(CursorSchema.parse('abc')).toBe('abc'); + expect(InitializeRequestSchema.safeParse({}).success).toBe(false); + }); + + it('re-exports every *Schema declared in core (drift guard)', () => { + // If core gains a new spec schema, this fails until it is added to src/index.ts. + // (Renames/removals are already caught by typecheck — the named re-export would not resolve.) + const src = readFileSync(fileURLToPath(new URL('../../core/src/types/schemas.ts', import.meta.url)), 'utf8'); + const coreSchemas = [...src.matchAll(/^export const (\w+Schema)\b/gm)] + .map(m => m[1]) + .filter((name): name is string => name !== undefined); + const exported = new Set(Object.keys(sdkShared)); + expect(coreSchemas.filter(name => !exported.has(name))).toEqual([]); + expect(coreSchemas.length).toBeGreaterThanOrEqual(159); + }); +}); diff --git a/packages/sdk-shared/tsconfig.json b/packages/sdk-shared/tsconfig.json new file mode 100644 index 0000000000..7a1fa16622 --- /dev/null +++ b/packages/sdk-shared/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "@modelcontextprotocol/tsconfig", + "include": ["./"], + "exclude": ["node_modules", "dist"], + "compilerOptions": { + "paths": { + "*": ["./*"], + "@modelcontextprotocol/core": ["./node_modules/@modelcontextprotocol/core/src/types/schemas.ts"] + } + } +} diff --git a/packages/sdk-shared/tsdown.config.ts b/packages/sdk-shared/tsdown.config.ts new file mode 100644 index 0000000000..7fe5a320db --- /dev/null +++ b/packages/sdk-shared/tsdown.config.ts @@ -0,0 +1,28 @@ +import { defineConfig } from 'tsdown'; + +// sdk-shared re-exports ONLY the spec Zod schemas from @modelcontextprotocol/core (private, +// unpublished). The core specifier is aliased to core's schemas module (core/src/types/schemas.ts) +// rather than its barrel, so the bundled graph is just the schemas + the constants they use — +// never Protocol, transports, stdio, or the ajv/cfWorker validators. `platform: 'neutral'` keeps +// the output runtime-neutral: a node-only dependency leaking into the graph would fail the build +// here instead of silently shipping. +export default defineConfig({ + failOnWarn: 'ci-only', + entry: ['src/index.ts'], + format: ['esm'], + outDir: 'dist', + clean: true, + sourcemap: true, + target: 'esnext', + platform: 'neutral', + dts: { + resolver: 'tsc', + compilerOptions: { + baseUrl: '.', + paths: { + '@modelcontextprotocol/core': ['../core/src/types/schemas.ts'] + } + } + }, + noExternal: ['@modelcontextprotocol/core'] +}); diff --git a/packages/sdk-shared/typedoc.json b/packages/sdk-shared/typedoc.json new file mode 100644 index 0000000000..08e5572417 --- /dev/null +++ b/packages/sdk-shared/typedoc.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://typedoc.org/schema.json", + "entryPoints": ["src/index.ts"] +} diff --git a/packages/sdk-shared/vitest.config.js b/packages/sdk-shared/vitest.config.js new file mode 100644 index 0000000000..496fca3200 --- /dev/null +++ b/packages/sdk-shared/vitest.config.js @@ -0,0 +1,3 @@ +import baseConfig from '@modelcontextprotocol/vitest-config'; + +export default baseConfig; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9ffd38d3dd..f6f566d114 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -257,7 +257,7 @@ importers: version: 4.4.4(eslint-plugin-import@2.32.0)(eslint@9.39.4) eslint-plugin-import: specifier: ^2.32.0 - version: 2.32.0(@typescript-eslint/parser@8.57.2(eslint@9.39.4)(typescript@5.9.3))(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.4) + version: 2.32.0(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.4) eslint-plugin-n: specifier: catalog:devTools version: 17.24.0(eslint@9.39.4)(typescript@5.9.3) @@ -938,6 +938,55 @@ importers: specifier: catalog:devTools version: 4.1.2(@opentelemetry/api@1.9.1)(@types/node@25.5.0)(vite@7.3.0(@types/node@25.5.0)(tsx@4.21.0)(yaml@2.8.3)) + packages/sdk-shared: + dependencies: + zod: + specifier: catalog:runtimeShared + version: 4.3.6 + devDependencies: + '@eslint/js': + specifier: catalog:devTools + version: 9.39.4 + '@modelcontextprotocol/core': + specifier: workspace:^ + version: link:../core + '@modelcontextprotocol/eslint-config': + specifier: workspace:^ + version: link:../../common/eslint-config + '@modelcontextprotocol/tsconfig': + specifier: workspace:^ + version: link:../../common/tsconfig + '@modelcontextprotocol/vitest-config': + specifier: workspace:^ + version: link:../../common/vitest-config + '@typescript/native-preview': + specifier: catalog:devTools + version: 7.0.0-dev.20260327.2 + eslint: + specifier: catalog:devTools + version: 9.39.4 + eslint-config-prettier: + specifier: catalog:devTools + version: 10.1.8(eslint@9.39.4) + eslint-plugin-n: + specifier: catalog:devTools + version: 17.24.0(eslint@9.39.4)(typescript@5.9.3) + prettier: + specifier: catalog:devTools + version: 3.6.2 + tsdown: + specifier: catalog:devTools + version: 0.18.4(@typescript/native-preview@7.0.0-dev.20260327.2)(typescript@5.9.3) + typescript: + specifier: catalog:devTools + version: 5.9.3 + typescript-eslint: + specifier: catalog:devTools + version: 8.57.2(eslint@9.39.4)(typescript@5.9.3) + vitest: + specifier: catalog:devTools + version: 4.1.2(@opentelemetry/api@1.9.1)(@types/node@25.5.0)(vite@7.3.0(@types/node@25.5.0)(tsx@4.21.0)(yaml@2.8.3)) + packages/server: dependencies: zod: @@ -7352,15 +7401,14 @@ snapshots: tinyglobby: 0.2.15 unrs-resolver: 1.11.1 optionalDependencies: - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.57.2(eslint@9.39.4)(typescript@5.9.3))(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.4) + eslint-plugin-import: 2.32.0(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.4) transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.1(@typescript-eslint/parser@8.57.2(eslint@9.39.4)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.4): + eslint-module-utils@2.12.1(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.4): dependencies: debug: 3.2.7 optionalDependencies: - '@typescript-eslint/parser': 8.57.2(eslint@9.39.4)(typescript@5.9.3) eslint: 9.39.4 eslint-import-resolver-node: 0.3.9 eslint-import-resolver-typescript: 4.4.4(eslint-plugin-import@2.32.0)(eslint@9.39.4) @@ -7374,7 +7422,7 @@ snapshots: eslint: 9.39.4 eslint-compat-utils: 0.5.1(eslint@9.39.4) - eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.57.2(eslint@9.39.4)(typescript@5.9.3))(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.4): + eslint-plugin-import@2.32.0(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.4): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9 @@ -7385,7 +7433,7 @@ snapshots: doctrine: 2.1.0 eslint: 9.39.4 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.57.2(eslint@9.39.4)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.4) + eslint-module-utils: 2.12.1(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.4) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -7396,8 +7444,6 @@ snapshots: semver: 6.3.1 string.prototype.trimend: 1.0.9 tsconfig-paths: 3.15.0 - optionalDependencies: - '@typescript-eslint/parser': 8.57.2(eslint@9.39.4)(typescript@5.9.3) transitivePeerDependencies: - eslint-import-resolver-typescript - eslint-import-resolver-webpack diff --git a/typedoc.config.mjs b/typedoc.config.mjs index f2a4e50f56..ee7bcc663d 100644 --- a/typedoc.config.mjs +++ b/typedoc.config.mjs @@ -3,10 +3,12 @@ import fg from 'fast-glob'; import { readFileSync } from 'node:fs'; import { join } from 'node:path'; -// Find all package.json files under packages/ and build package list +// Find all package.json files under packages/ and build package list. +// Exclude node_modules and the codemod batch-test's cloned real-world repos, which are not part +// of this SDK's public API surface (and would otherwise fail docs:check locally when present). const packageJsonPaths = await fg('packages/**/package.json', { cwd: process.cwd(), - ignore: ['**/node_modules/**'] + ignore: ['**/node_modules/**', '**/batch-test/**'] }); const packages = packageJsonPaths.map(p => { const rootDir = join(process.cwd(), p.replace('/package.json', '')); @@ -45,6 +47,14 @@ export default { readme: false }, customJs: 'docs/v2-banner.js', + // The spec-generated schema/type JSDoc uses `{@linkcode | method}` cross-references. + // With the data model split across packages (Zod schemas in @modelcontextprotocol/sdk-shared, + // their types in @modelcontextprotocol/server / -client), typedoc's per-package link resolution + // can't resolve those bare cross-package references. Disable only the invalid-link check; every + // other validation (notExported, etc.) stays on under treatWarningsAsErrors. + validation: { + invalidLink: false + }, treatWarningsAsErrors: true, out: 'tmp/docs/', externalSymbolLinkMappings: { From c65285ad0c036c972f0ca6ed4c0b8d1eb0ef8990 Mon Sep 17 00:00:00 2001 From: Konstantin Konstantinov Date: Wed, 24 Jun 2026 10:40:27 +0300 Subject: [PATCH 04/21] fix(codemod): infer client/server project type from v1 source Shared protocol types/constants resolve to either @modelcontextprotocol/client or /server. The codemod read that choice from package.json, but a project mid-migration still declares the single v1 @modelcontextprotocol/sdk dependency, so the project type came back 'unknown' and every file importing only shared symbols defaulted to server with an action-required warning. Infer from source instead: when the v2 split deps are absent, scan for quoted @modelcontextprotocol/sdk/client/ and .../server/ import specifiers (both -> 'both', one -> that side, neither -> 'unknown'). Matching quoted specifiers rather than bare substrings ignores comments/prose and catches extensionless/bare subpaths. For a 'both' project, shared types resolve to server with an info note (both re-export them) instead of an action-required warning; 'unknown' still warns. The scan is bounded (skips heavy dirs, file budget, early-exit). On firebase this cuts the codemod diagnostics from 14 to 2 with 0 introduced typecheck errors. --- .changeset/codemod-infer-project-type.md | 5 + packages/codemod/src/utils/projectAnalyzer.ts | 122 +++++++++++++++--- packages/codemod/test/projectAnalyzer.test.ts | 97 +++++++++++++- 3 files changed, 205 insertions(+), 19 deletions(-) create mode 100644 .changeset/codemod-infer-project-type.md diff --git a/.changeset/codemod-infer-project-type.md b/.changeset/codemod-infer-project-type.md new file mode 100644 index 0000000000..1fa114c775 --- /dev/null +++ b/.changeset/codemod-infer-project-type.md @@ -0,0 +1,5 @@ +--- +'@modelcontextprotocol/codemod': patch +--- + +Infer client/server project type from source for v1 projects. A project being migrated still declares the single v1 `@modelcontextprotocol/sdk` dependency, so detecting the project type from `package.json` came back "unknown" and every file importing only shared protocol symbols defaulted to `@modelcontextprotocol/server` with an action-required warning. The codemod now scans the source for quoted `@modelcontextprotocol/sdk/client/…` and `…/server/…` import specifiers to infer the type (both → "both", one → that side, neither → "unknown"), routing shared symbols to the installed package and replacing the spurious warnings with at most an info note for genuinely ambiguous "both" projects. diff --git a/packages/codemod/src/utils/projectAnalyzer.ts b/packages/codemod/src/utils/projectAnalyzer.ts index daf4088876..7815e8708e 100644 --- a/packages/codemod/src/utils/projectAnalyzer.ts +++ b/packages/codemod/src/utils/projectAnalyzer.ts @@ -1,11 +1,24 @@ -import { existsSync, readFileSync } from 'node:fs'; +import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs'; import path from 'node:path'; import type { Diagnostic, TransformContext } from '../types.js'; -import { warning } from './diagnostics.js'; +import { info, warning } from './diagnostics.js'; const PROJECT_ROOT_MARKERS = ['.git', 'node_modules']; +const SCAN_EXTENSIONS = new Set(['.ts', '.tsx', '.mts', '.cts', '.js', '.jsx', '.mjs', '.cjs']); +const SCAN_SKIP_DIRS = new Set(['node_modules', 'dist', '.git', 'build', '.next', '.nuxt', 'coverage']); +const SCAN_FILE_BUDGET = 5000; + +// Matches a quoted v1 SDK client/server subpath import specifier — e.g. +// '@modelcontextprotocol/sdk/client/index.js' "@modelcontextprotocol/sdk/server/mcp.js" +// '@modelcontextprotocol/sdk/client' (extensionless / bare subpath; see the extensionless +// import matching the codemod already supports) +// Anchored to the opening quote and a trailing `/` or closing quote so that comments or prose that +// merely mention the path do not count, and `…/client` is not confused with `…/clientfoo`. +const CLIENT_IMPORT_RE = /['"`]@modelcontextprotocol\/sdk\/client(?:\/|['"`])/; +const SERVER_IMPORT_RE = /['"`]@modelcontextprotocol\/sdk\/server(?:\/|['"`])/; + export function findPackageJson(startDir: string): string | undefined { let dir = path.resolve(startDir); const root = path.parse(dir).root; @@ -20,27 +33,86 @@ export function findPackageJson(startDir: string): string | undefined { export function analyzeProject(targetDir: string): TransformContext { const pkgJsonPath = findPackageJson(targetDir); - if (!pkgJsonPath) { - return { projectType: 'unknown' }; + if (pkgJsonPath) { + try { + const pkgJson = JSON.parse(readFileSync(pkgJsonPath, 'utf8')); + const allDeps = { + ...pkgJson.dependencies, + ...pkgJson.devDependencies + }; + + const hasClient = '@modelcontextprotocol/client' in allDeps; + const hasServer = '@modelcontextprotocol/server' in allDeps; + + if (hasClient && hasServer) return { projectType: 'both' }; + if (hasClient) return { projectType: 'client' }; + if (hasServer) return { projectType: 'server' }; + // No v2 split deps — this is almost always a v1 project mid-migration (v1 ships as the single + // `@modelcontextprotocol/sdk` package). Fall through to inferring the type from source usage. + } catch { + // Malformed package.json — fall through to source inference. + } } - try { - const pkgJson = JSON.parse(readFileSync(pkgJsonPath, 'utf8')); - const allDeps = { - ...pkgJson.dependencies, - ...pkgJson.devDependencies - }; + return { projectType: inferProjectTypeFromSource(targetDir) }; +} - const hasClient = '@modelcontextprotocol/client' in allDeps; - const hasServer = '@modelcontextprotocol/server' in allDeps; +/** + * Infer client vs server vs both by scanning the source for v1 SDK subpath imports: a + * `@modelcontextprotocol/sdk/client/...` specifier means the project will need + * `@modelcontextprotocol/client`; a `.../server/...` specifier means it needs `@modelcontextprotocol/server`. + * Files that import only shared paths (`types.js`, `shared/...`) give no signal. The scan matches quoted + * specifiers (not bare substrings), so comments/prose are ignored. Bounded: skips heavy dirs, caps the + * file count, and early-exits once both signals are seen. + */ +function inferProjectTypeFromSource(targetDir: string): TransformContext['projectType'] { + let usesClient = false; + let usesServer = false; + let scanned = 0; - if (hasClient && hasServer) return { projectType: 'both' }; - if (hasClient) return { projectType: 'client' }; - if (hasServer) return { projectType: 'server' }; - return { projectType: 'unknown' }; + const visit = (dir: string): void => { + if (usesClient && usesServer) return; + let entries: import('node:fs').Dirent[]; + try { + entries = readdirSync(dir, { withFileTypes: true }); + } catch { + return; + } + for (const entry of entries) { + if (usesClient && usesServer) return; + const full = path.join(dir, entry.name); + if (entry.isDirectory()) { + if (SCAN_SKIP_DIRS.has(entry.name)) continue; + visit(full); + } else if (entry.isFile()) { + const ext = path.extname(entry.name); + if (!SCAN_EXTENSIONS.has(ext) || entry.name.endsWith('.d.ts')) continue; + if (scanned >= SCAN_FILE_BUDGET) return; + scanned++; + let content: string; + try { + content = readFileSync(full, 'utf8'); + } catch { + continue; + } + if (!usesClient && CLIENT_IMPORT_RE.test(content)) usesClient = true; + if (!usesServer && SERVER_IMPORT_RE.test(content)) usesServer = true; + } + } + }; + + let root = targetDir; + try { + if (!statSync(targetDir).isDirectory()) root = path.dirname(targetDir); } catch { - return { projectType: 'unknown' }; + return 'unknown'; } + visit(root); + + if (usesClient && usesServer) return 'both'; + if (usesClient) return 'client'; + if (usesServer) return 'server'; + return 'unknown'; } export function resolveTypesPackage( @@ -61,6 +133,22 @@ export function resolveTypesPackage( if (context.projectType === 'server') { return '@modelcontextprotocol/server'; } + if (context.projectType === 'both') { + // Both packages are present and both re-export the shared protocol types (from core), so importing + // from either compiles. This file has no client/server-specific signal — default to server and note + // it as an optional preference, not an action-required warning. + if (diagnosticSink) { + diagnosticSink.diagnostics.push( + info( + diagnosticSink.filePath, + diagnosticSink.line, + 'Shared protocol types imported from @modelcontextprotocol/server (both client and server ' + + 're-export them). Switch to @modelcontextprotocol/client if this is client-only code.' + ) + ); + } + return '@modelcontextprotocol/server'; + } if (diagnosticSink) { diagnosticSink.diagnostics.push( warning( diff --git a/packages/codemod/test/projectAnalyzer.test.ts b/packages/codemod/test/projectAnalyzer.test.ts index 0f69eacf79..1eb3c6ee6f 100644 --- a/packages/codemod/test/projectAnalyzer.test.ts +++ b/packages/codemod/test/projectAnalyzer.test.ts @@ -3,7 +3,7 @@ import { tmpdir } from 'node:os'; import path from 'node:path'; import { describe, it, expect, afterEach } from 'vitest'; -import { analyzeProject } from '../src/utils/projectAnalyzer.js'; +import { analyzeProject, resolveTypesPackage } from '../src/utils/projectAnalyzer.js'; let tempDir: string; @@ -115,7 +115,7 @@ describe('analyzeProject', () => { expect(result.projectType).toBe('server'); }); - it('returns unknown for v1 SDK package (falls through to per-file resolution)', () => { + it('returns unknown for a v1 SDK package with no source signal', () => { const dir = createTempDir(); writeFileSync( path.join(dir, 'package.json'), @@ -127,4 +127,97 @@ describe('analyzeProject', () => { const result = analyzeProject(dir); expect(result.projectType).toBe('unknown'); }); + + describe('source inference for v1 (pre-split) projects', () => { + function v1Project(files: Record): string { + const dir = createTempDir(); + writeFileSync(path.join(dir, 'package.json'), JSON.stringify({ dependencies: { '@modelcontextprotocol/sdk': '^1.0.0' } })); + mkdirSync(path.join(dir, 'src'), { recursive: true }); + for (const [name, content] of Object.entries(files)) { + writeFileSync(path.join(dir, 'src', name), content); + } + return dir; + } + + it('infers client from sdk/client subpath usage', () => { + const dir = v1Project({ 'a.ts': `import { Client } from '@modelcontextprotocol/sdk/client/index.js';` }); + expect(analyzeProject(dir).projectType).toBe('client'); + }); + + it('infers server from sdk/server subpath usage', () => { + const dir = v1Project({ 'a.ts': `import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';` }); + expect(analyzeProject(dir).projectType).toBe('server'); + }); + + it('infers both when client and server subpaths are used across files', () => { + const dir = v1Project({ + 'client.ts': `import { Client } from '@modelcontextprotocol/sdk/client/index.js';`, + 'server.ts': `import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';` + }); + expect(analyzeProject(dir).projectType).toBe('both'); + }); + + it('infers from an extensionless / bare sdk subpath specifier', () => { + const dir = v1Project({ 'a.ts': `import { McpServer } from '@modelcontextprotocol/sdk/server';` }); + expect(analyzeProject(dir).projectType).toBe('server'); + }); + + it('stays unknown when only shared paths are imported', () => { + const dir = v1Project({ 'a.ts': `import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';` }); + expect(analyzeProject(dir).projectType).toBe('unknown'); + }); + + it('ignores an import path that appears only in a comment (not a quoted specifier)', () => { + // A real client import plus a comment mentioning the server subpath. A whole-file substring + // scan would flip this to "both"; the quote-anchored match keeps it "client". + const dir = v1Project({ + 'a.ts': [ + `import { Client } from '@modelcontextprotocol/sdk/client/index.js';`, + `// previously imported from @modelcontextprotocol/sdk/server/mcp.js`, + '' + ].join('\n') + }); + expect(analyzeProject(dir).projectType).toBe('client'); + }); + + it('infers from source even without a package.json', () => { + const dir = createTempDir(); + mkdirSync(path.join(dir, 'src'), { recursive: true }); + writeFileSync(path.join(dir, 'src', 'a.ts'), `import { Client } from '@modelcontextprotocol/sdk/client/index.js';`); + expect(analyzeProject(path.join(dir, 'src')).projectType).toBe('client'); + }); + + it('ignores node_modules when scanning source', () => { + const dir = v1Project({ 'a.ts': `import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';` }); + mkdirSync(path.join(dir, 'node_modules', 'pkg'), { recursive: true }); + writeFileSync( + path.join(dir, 'node_modules', 'pkg', 'index.ts'), + `import { Client } from '@modelcontextprotocol/sdk/client/index.js';` + ); + // Only the server import in src counts; the client import under node_modules is skipped. + expect(analyzeProject(dir).projectType).toBe('server'); + }); + }); +}); + +describe('resolveTypesPackage', () => { + it('emits an info note (not a warning) for a both-project ambiguous file', () => { + const sink = { filePath: 'f.ts', line: 1, diagnostics: [] as import('../src/types.js').Diagnostic[] }; + const target = resolveTypesPackage({ projectType: 'both' }, false, false, sink); + expect(target).toBe('@modelcontextprotocol/server'); + expect(sink.diagnostics).toHaveLength(1); + expect(sink.diagnostics[0]!.level).toBe('info'); + }); + + it('emits an action-required warning for a genuinely unknown project', () => { + const sink = { filePath: 'f.ts', line: 1, diagnostics: [] as import('../src/types.js').Diagnostic[] }; + resolveTypesPackage({ projectType: 'unknown' }, false, false, sink); + expect(sink.diagnostics).toHaveLength(1); + expect(sink.diagnostics[0]!.level).toBe('warning'); + }); + + it('resolves by per-file signal regardless of project type', () => { + expect(resolveTypesPackage({ projectType: 'both' }, true, false)).toBe('@modelcontextprotocol/client'); + expect(resolveTypesPackage({ projectType: 'unknown' }, false, true)).toBe('@modelcontextprotocol/server'); + }); }); From 7d5aac35850fba59391a29ffc5ac0ddf404ce23b Mon Sep 17 00:00:00 2001 From: Konstantin Konstantinov Date: Wed, 24 Jun 2026 10:52:05 +0300 Subject: [PATCH 05/21] feat(codemod): map task request/notification schemas to method strings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The handler-registration transform rewrites setRequestHandler(XSchema, …) and setNotificationHandler(XSchema, …) to the v2 spec form via a schema→method table. The task schemas were missing, so a handler like setNotificationHandler(TaskStatusNotificationSchema, …) fell through to the generic "use the 3-argument form" diagnostic and was left for manual migration. Add the task entries: tasks/get, tasks/result, tasks/list, tasks/cancel, and notifications/tasks/status. These are spec methods (the request schemas are members of ServerRequestSchema and the notification is in the notification union), so the rewritten two-argument call resolves to the spec overload of setRequestHandler/setNotificationHandler and typechecks. Co-Authored-By: Felix Weinberger --- .changeset/codemod-task-handler-methods.md | 5 +++++ .../v1-to-v2/mappings/schemaToMethodMap.ts | 9 ++++++-- .../transforms/handlerRegistration.test.ts | 22 +++++++++++++++++++ 3 files changed, 34 insertions(+), 2 deletions(-) create mode 100644 .changeset/codemod-task-handler-methods.md diff --git a/.changeset/codemod-task-handler-methods.md b/.changeset/codemod-task-handler-methods.md new file mode 100644 index 0000000000..5ed163d21a --- /dev/null +++ b/.changeset/codemod-task-handler-methods.md @@ -0,0 +1,5 @@ +--- +'@modelcontextprotocol/codemod': patch +--- + +Map the task request/notification schemas to their v2 method strings in the handler-registration transform. `setRequestHandler(GetTaskRequestSchema, …)`, `setNotificationHandler(TaskStatusNotificationSchema, …)`, and the other task handlers (`tasks/get`, `tasks/result`, `tasks/list`, `tasks/cancel`, `notifications/tasks/status`) now rewrite to the v2 two-argument method-string form instead of falling through to the generic "use the 3-argument form" manual-migration diagnostic. diff --git a/packages/codemod/src/migrations/v1-to-v2/mappings/schemaToMethodMap.ts b/packages/codemod/src/migrations/v1-to-v2/mappings/schemaToMethodMap.ts index daa7278c8f..783cf52875 100644 --- a/packages/codemod/src/migrations/v1-to-v2/mappings/schemaToMethodMap.ts +++ b/packages/codemod/src/migrations/v1-to-v2/mappings/schemaToMethodMap.ts @@ -14,7 +14,11 @@ export const SCHEMA_TO_METHOD: Record = { SetLevelRequestSchema: 'logging/setLevel', PingRequestSchema: 'ping', CompleteRequestSchema: 'completion/complete', - ListRootsRequestSchema: 'roots/list' + ListRootsRequestSchema: 'roots/list', + GetTaskRequestSchema: 'tasks/get', + GetTaskPayloadRequestSchema: 'tasks/result', + ListTasksRequestSchema: 'tasks/list', + CancelTaskRequestSchema: 'tasks/cancel' }; export const NOTIFICATION_SCHEMA_TO_METHOD: Record = { @@ -27,5 +31,6 @@ export const NOTIFICATION_SCHEMA_TO_METHOD: Record = { CancelledNotificationSchema: 'notifications/cancelled', InitializedNotificationSchema: 'notifications/initialized', RootsListChangedNotificationSchema: 'notifications/roots/list_changed', - ElicitationCompleteNotificationSchema: 'notifications/elicitation/complete' + ElicitationCompleteNotificationSchema: 'notifications/elicitation/complete', + TaskStatusNotificationSchema: 'notifications/tasks/status' }; 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 e4602910de..741d2f77d0 100644 --- a/packages/codemod/test/v1-to-v2/transforms/handlerRegistration.test.ts +++ b/packages/codemod/test/v1-to-v2/transforms/handlerRegistration.test.ts @@ -219,6 +219,28 @@ describe('handler-registration transform', () => { expect(result).not.toContain('ElicitationCompleteNotificationSchema'); }); + it('replaces TaskStatusNotificationSchema with the tasks/status method string', () => { + const input = [ + `import { TaskStatusNotificationSchema } from '@modelcontextprotocol/sdk/types.js';`, + `client.setNotificationHandler(TaskStatusNotificationSchema, async () => {});`, + '' + ].join('\n'); + const result = applyTransform(input); + expect(result).toContain("setNotificationHandler('notifications/tasks/status'"); + expect(result).not.toContain('TaskStatusNotificationSchema'); + }); + + it('replaces task request schemas (GetTaskRequestSchema → tasks/get)', () => { + const input = [ + `import { GetTaskRequestSchema } from '@modelcontextprotocol/sdk/types.js';`, + `server.setRequestHandler(GetTaskRequestSchema, async () => ({}));`, + '' + ].join('\n'); + const result = applyTransform(input); + expect(result).toContain("setRequestHandler('tasks/get'"); + expect(result).not.toContain('GetTaskRequestSchema'); + }); + it('does not emit diagnostic when first arg is a string literal (v2 style)', () => { const input = [`server.setRequestHandler('tools/call', async (request) => {`, ` return { content: [] };`, `});`, ''].join('\n'); const project = new Project({ useInMemoryFileSystem: true }); From 5ffdb386bcf50a88c764e54149b79d049c4fc38d Mon Sep 17 00:00:00 2001 From: Konstantin Konstantinov Date: Wed, 24 Jun 2026 16:19:57 +0300 Subject: [PATCH 06/21] fixes --- .../migrations/v1-to-v2/mappings/importMap.ts | 11 +- .../v1-to-v2/mappings/specSchemaNames.ts | 164 ++++++++++++++++++ .../v1-to-v2/transforms/importPaths.ts | 18 +- .../test/v1-to-v2/specSchemaNames.test.ts | 21 +++ .../v1-to-v2/transforms/importPaths.test.ts | 59 +++++++ packages/sdk-shared/src/index.ts | 13 +- .../sdk-shared/test/sdkSharedSchemas.test.ts | 27 ++- 7 files changed, 291 insertions(+), 22 deletions(-) create mode 100644 packages/codemod/src/migrations/v1-to-v2/mappings/specSchemaNames.ts create mode 100644 packages/codemod/test/v1-to-v2/specSchemaNames.test.ts diff --git a/packages/codemod/src/migrations/v1-to-v2/mappings/importMap.ts b/packages/codemod/src/migrations/v1-to-v2/mappings/importMap.ts index 04366c7dd4..d06d8f37ca 100644 --- a/packages/codemod/src/migrations/v1-to-v2/mappings/importMap.ts +++ b/packages/codemod/src/migrations/v1-to-v2/mappings/importMap.ts @@ -5,10 +5,13 @@ export interface ImportMapping { /** Route specific symbols to a different target package than `target`. */ symbolTargetOverrides?: Record; /** - * Route every imported symbol whose name ends in `Schema` to this package, instead of `target`. - * Used for `sdk/types.js`: the spec Zod schemas now live in `@modelcontextprotocol/sdk-shared` - * (so `Schema.parse(...)` keeps working), while the spec types/constants resolve by context. - * `symbolTargetOverrides` (exact-name) takes precedence over this suffix rule. + * Route an imported symbol to this package (instead of `target`) when its rename-resolved name is + * an actual spec schema constant — a member of `SPEC_SCHEMA_NAMES`. Used for `sdk/types.js`: the + * spec Zod schemas now live in `@modelcontextprotocol/sdk-shared` (so `Schema.parse(...)` + * keeps working), while spec types/constants/guards resolve by context. Matching on membership + * (not a `*Schema` suffix) keeps spec TYPES whose name ends in `Schema` — e.g. the elicitation + * primitives `BooleanSchema`/`StringSchema`/`EnumSchema` — routed by context, where their types + * live. `symbolTargetOverrides` (exact-name) takes precedence. */ schemaSymbolTarget?: string; removalMessage?: string; diff --git a/packages/codemod/src/migrations/v1-to-v2/mappings/specSchemaNames.ts b/packages/codemod/src/migrations/v1-to-v2/mappings/specSchemaNames.ts new file mode 100644 index 0000000000..3007eaea8d --- /dev/null +++ b/packages/codemod/src/migrations/v1-to-v2/mappings/specSchemaNames.ts @@ -0,0 +1,164 @@ +// AUTO-VERIFIED against @modelcontextprotocol/sdk-shared's public exports by +// test/v1-to-v2/specSchemaNames.test.ts (drift guard). These are the spec Zod schema CONSTANTS that +// sdk-shared re-exports as standalone values; the v1->v2 import transform routes a `*Schema` symbol +// imported from `@modelcontextprotocol/sdk/types.js` to sdk-shared ONLY when its (rename-resolved) +// name is in this set. Names that merely END in `Schema` but are NOT here — e.g. the elicitation +// primitive TYPES `BooleanSchema`/`StringSchema`/`EnumSchema` (whose Zod const is `SchemaSchema`) +// — fall through to context resolution (@modelcontextprotocol/client | /server), where their TYPES +// live. Keep alphabetized. +export const SPEC_SCHEMA_NAMES: ReadonlySet = new Set([ + 'AnnotationsSchema', + 'AudioContentSchema', + 'BaseMetadataSchema', + 'BlobResourceContentsSchema', + 'BooleanSchemaSchema', + 'CallToolRequestParamsSchema', + 'CallToolRequestSchema', + 'CallToolResultSchema', + 'CancelTaskRequestSchema', + 'CancelTaskResultSchema', + 'CancelledNotificationParamsSchema', + 'CancelledNotificationSchema', + 'ClientCapabilitiesSchema', + 'ClientNotificationSchema', + 'ClientRequestSchema', + 'ClientResultSchema', + 'CompatibilityCallToolResultSchema', + 'CompleteRequestParamsSchema', + 'CompleteRequestSchema', + 'CompleteResultSchema', + 'ContentBlockSchema', + 'CreateMessageRequestParamsSchema', + 'CreateMessageRequestSchema', + 'CreateMessageResultSchema', + 'CreateMessageResultWithToolsSchema', + 'CreateTaskResultSchema', + 'CursorSchema', + 'DiscoverRequestSchema', + 'DiscoverResultSchema', + 'ElicitRequestFormParamsSchema', + 'ElicitRequestParamsSchema', + 'ElicitRequestSchema', + 'ElicitRequestURLParamsSchema', + 'ElicitResultSchema', + 'ElicitationCompleteNotificationParamsSchema', + 'ElicitationCompleteNotificationSchema', + 'EmbeddedResourceSchema', + 'EmptyResultSchema', + 'EnumSchemaSchema', + 'GetPromptRequestParamsSchema', + 'GetPromptRequestSchema', + 'GetPromptResultSchema', + 'GetTaskPayloadRequestSchema', + 'GetTaskPayloadResultSchema', + 'GetTaskRequestSchema', + 'GetTaskResultSchema', + 'IconSchema', + 'IconsSchema', + 'ImageContentSchema', + 'ImplementationSchema', + 'InitializeRequestParamsSchema', + 'InitializeRequestSchema', + 'InitializeResultSchema', + 'InitializedNotificationSchema', + 'JSONArraySchema', + 'JSONObjectSchema', + 'JSONRPCErrorResponseSchema', + 'JSONRPCMessageSchema', + 'JSONRPCNotificationSchema', + 'JSONRPCRequestSchema', + 'JSONRPCResponseSchema', + 'JSONRPCResultResponseSchema', + 'JSONValueSchema', + 'LegacyTitledEnumSchemaSchema', + 'ListPromptsRequestSchema', + 'ListPromptsResultSchema', + 'ListResourceTemplatesRequestSchema', + 'ListResourceTemplatesResultSchema', + 'ListResourcesRequestSchema', + 'ListResourcesResultSchema', + 'ListRootsRequestSchema', + 'ListRootsResultSchema', + 'ListTasksRequestSchema', + 'ListTasksResultSchema', + 'ListToolsRequestSchema', + 'ListToolsResultSchema', + 'LoggingLevelSchema', + 'LoggingMessageNotificationParamsSchema', + 'LoggingMessageNotificationSchema', + 'ModelHintSchema', + 'ModelPreferencesSchema', + 'MultiSelectEnumSchemaSchema', + 'NotificationSchema', + 'NumberSchemaSchema', + 'PaginatedRequestParamsSchema', + 'PaginatedRequestSchema', + 'PaginatedResultSchema', + 'PingRequestSchema', + 'PrimitiveSchemaDefinitionSchema', + 'ProgressNotificationParamsSchema', + 'ProgressNotificationSchema', + 'ProgressSchema', + 'ProgressTokenSchema', + 'PromptArgumentSchema', + 'PromptListChangedNotificationSchema', + 'PromptMessageSchema', + 'PromptReferenceSchema', + 'PromptSchema', + 'ReadResourceRequestParamsSchema', + 'ReadResourceRequestSchema', + 'ReadResourceResultSchema', + 'RelatedTaskMetadataSchema', + 'RequestIdSchema', + 'RequestMetaEnvelopeSchema', + 'RequestMetaSchema', + 'RequestSchema', + 'ResourceContentsSchema', + 'ResourceLinkSchema', + 'ResourceListChangedNotificationSchema', + 'ResourceRequestParamsSchema', + 'ResourceSchema', + 'ResourceTemplateReferenceSchema', + 'ResourceTemplateSchema', + 'ResourceUpdatedNotificationParamsSchema', + 'ResourceUpdatedNotificationSchema', + 'ResultSchema', + 'RoleSchema', + 'RootSchema', + 'RootsListChangedNotificationSchema', + 'SamplingContentSchema', + 'SamplingMessageContentBlockSchema', + 'SamplingMessageSchema', + 'ServerCapabilitiesSchema', + 'ServerNotificationSchema', + 'ServerRequestSchema', + 'ServerResultSchema', + 'SetLevelRequestParamsSchema', + 'SetLevelRequestSchema', + 'SingleSelectEnumSchemaSchema', + 'StringSchemaSchema', + 'SubscribeRequestParamsSchema', + 'SubscribeRequestSchema', + 'TaskAugmentedRequestParamsSchema', + 'TaskCreationParamsSchema', + 'TaskMetadataSchema', + 'TaskSchema', + 'TaskStatusNotificationParamsSchema', + 'TaskStatusNotificationSchema', + 'TaskStatusSchema', + 'TextContentSchema', + 'TextResourceContentsSchema', + 'TitledMultiSelectEnumSchemaSchema', + 'TitledSingleSelectEnumSchemaSchema', + 'ToolAnnotationsSchema', + 'ToolChoiceSchema', + 'ToolExecutionSchema', + 'ToolListChangedNotificationSchema', + 'ToolResultContentSchema', + 'ToolSchema', + 'ToolUseContentSchema', + 'UnsubscribeRequestParamsSchema', + 'UnsubscribeRequestSchema', + 'UntitledMultiSelectEnumSchemaSchema', + 'UntitledSingleSelectEnumSchemaSchema' +]); diff --git a/packages/codemod/src/migrations/v1-to-v2/transforms/importPaths.ts b/packages/codemod/src/migrations/v1-to-v2/transforms/importPaths.ts index 1215634460..fce2edf760 100644 --- a/packages/codemod/src/migrations/v1-to-v2/transforms/importPaths.ts +++ b/packages/codemod/src/migrations/v1-to-v2/transforms/importPaths.ts @@ -8,6 +8,7 @@ import { addOrMergeImport, getSdkExports, getSdkImports, isTypeOnlyImport } from import { resolveTypesPackage } from '../../../utils/projectAnalyzer.js'; import type { ImportMapping } from '../mappings/importMap.js'; import { isAuthImport, lookupImportMapping } from '../mappings/importMap.js'; +import { SPEC_SCHEMA_NAMES } from '../mappings/specSchemaNames.js'; import { SIMPLE_RENAMES } from '../mappings/symbolMap.js'; const REEXPORT_WARNINGS: Record = { @@ -19,16 +20,23 @@ const REEXPORT_WARNINGS: Record = { 'Re-exported StreamableHTTPError was renamed to SdkHttpError in v2 with a different constructor. Update this re-export manually.' }; +/** The v2 name a symbol resolves to after renames (per-mapping override, then global SIMPLE_RENAMES). */ +function resolveRenamedName(name: string, mapping: ImportMapping): string { + return mapping.renamedSymbols?.[name] ?? SIMPLE_RENAMES[name] ?? name; +} + /** * The per-symbol target package for a symbol imported/re-exported from `mapping`'s module, or * `undefined` when the symbol should use the mapping's resolved `target`. Exact-name - * `symbolTargetOverrides` win over the `schemaSymbolTarget` (`*Schema`) suffix rule. + * `symbolTargetOverrides` win over `schemaSymbolTarget`, which routes a symbol to the shared-schemas + * package only when its rename-resolved name is an actual spec schema constant (`SPEC_SCHEMA_NAMES`) — + * not merely any name ending in `Schema`, so spec TYPES such as `BooleanSchema` resolve by context. */ function symbolTargetOverride(name: string, mapping: ImportMapping): string | undefined { if (mapping.symbolTargetOverrides && name in mapping.symbolTargetOverrides) { return mapping.symbolTargetOverrides[name]; } - if (mapping.schemaSymbolTarget && name.endsWith('Schema')) { + if (mapping.schemaSymbolTarget && SPEC_SCHEMA_NAMES.has(resolveRenamedName(name, mapping))) { return mapping.schemaSymbolTarget; } return undefined; @@ -161,7 +169,11 @@ export const importPathsTransform: Transform = { ...new Set( sourceFile .getDescendantsOfKind(SyntaxKind.PropertyAccessExpression) - .filter(pa => pa.getExpression().getText() === nsName && pa.getName().endsWith('Schema')) + .filter( + pa => + pa.getExpression().getText() === nsName && + SPEC_SCHEMA_NAMES.has(resolveRenamedName(pa.getName(), mapping)) + ) .map(pa => pa.getName()) ) ]; diff --git a/packages/codemod/test/v1-to-v2/specSchemaNames.test.ts b/packages/codemod/test/v1-to-v2/specSchemaNames.test.ts new file mode 100644 index 0000000000..bd677aa7ed --- /dev/null +++ b/packages/codemod/test/v1-to-v2/specSchemaNames.test.ts @@ -0,0 +1,21 @@ +import { readFileSync } from 'node:fs'; +import { fileURLToPath } from 'node:url'; + +import { describe, expect, it } from 'vitest'; + +import { SPEC_SCHEMA_NAMES } from '../../src/migrations/v1-to-v2/mappings/specSchemaNames.js'; + +describe('SPEC_SCHEMA_NAMES (codemod schema-routing allowlist)', () => { + it("matches @modelcontextprotocol/sdk-shared's exported schema set exactly (drift guard)", () => { + // The import transform routes a `*Schema` symbol from sdk/types.js to sdk-shared only when the + // symbol's (rename-resolved) name is in this set. It must therefore equal sdk-shared's actual + // public exports: a name missing here would be misrouted to client/server (which export no Zod + // schema values), and a name here that sdk-shared does not export would produce a broken import. + // Read sdk-shared's barrel directly so the two cannot silently drift. + const src = readFileSync(fileURLToPath(new URL('../../../sdk-shared/src/index.ts', import.meta.url)), 'utf8'); + const block = src.slice(src.indexOf('export {') + 'export {'.length, src.indexOf('} from')); + const sdkSharedExports = [...new Set([...block.matchAll(/\b(\w+Schema)\b/g)].map(m => m[1]))].sort(); + expect([...SPEC_SCHEMA_NAMES].sort()).toEqual(sdkSharedExports); + expect(sdkSharedExports.length).toBeGreaterThanOrEqual(154); + }); +}); diff --git a/packages/codemod/test/v1-to-v2/transforms/importPaths.test.ts b/packages/codemod/test/v1-to-v2/transforms/importPaths.test.ts index c0569973a3..e58a699f89 100644 --- a/packages/codemod/test/v1-to-v2/transforms/importPaths.test.ts +++ b/packages/codemod/test/v1-to-v2/transforms/importPaths.test.ts @@ -142,6 +142,65 @@ describe('import-paths transform', () => { expect(result).toContain(`from "@modelcontextprotocol/sdk-shared"`); }); + it('routes elicitation primitive *Schema TYPE names from sdk/types.js by context, not to sdk-shared', () => { + // These names END in `Schema` but are TYPES; their Zod constant is `SchemaSchema`. They + // must resolve to the context package (where the types live), never to sdk-shared (which only + // exports the `*SchemaSchema` constants) — otherwise the codemod emits a broken import. + const elicitationTypeNames = [ + 'BooleanSchema', + 'StringSchema', + 'NumberSchema', + 'EnumSchema', + 'SingleSelectEnumSchema', + 'MultiSelectEnumSchema', + 'TitledSingleSelectEnumSchema', + 'UntitledSingleSelectEnumSchema', + 'TitledMultiSelectEnumSchema', + 'UntitledMultiSelectEnumSchema', + 'LegacyTitledEnumSchema' + ]; + for (const typeName of elicitationTypeNames) { + const input = `import { ${typeName} } from '@modelcontextprotocol/sdk/types.js';\n`; + const result = applyTransform(input, { projectType: 'server' }); + expect(result, typeName).toContain(`from "@modelcontextprotocol/server"`); + expect(result, typeName).not.toContain('@modelcontextprotocol/sdk-shared'); + expect(result, typeName).toContain(typeName); + } + }); + + it('splits a primitive-schema TYPE from its matching schema CONSTANT (BooleanSchema vs BooleanSchemaSchema)', () => { + // They differ only by a trailing `Schema`, which the suffix heuristic could not distinguish. + // The constant goes to sdk-shared; the type resolves by context. + const input = `import { BooleanSchema, BooleanSchemaSchema } from '@modelcontextprotocol/sdk/types.js';\n`; + const result = applyTransform(input, { projectType: 'server' }); + expect(result).toContain(`from "@modelcontextprotocol/sdk-shared"`); + expect(result).toContain('BooleanSchemaSchema'); + expect(result).toContain(`from "@modelcontextprotocol/server"`); + expect(result).toMatch(/BooleanSchema\b/); + expect(result).not.toContain('@modelcontextprotocol/sdk/types'); + }); + + it('routes a renamed spec schema (JSONRPCErrorSchema) from sdk/types.js to sdk-shared', () => { + // JSONRPCErrorSchema → JSONRPCErrorResponseSchema, a sdk-shared export. Membership is checked + // against the rename-resolved name; the symbolRenames transform applies the rename afterward, + // so importPaths alone leaves the name unchanged but routes it to sdk-shared. + const input = `import { JSONRPCErrorSchema } from '@modelcontextprotocol/sdk/types.js';\n`; + const result = applyTransform(input, { projectType: 'server' }); + expect(result).toContain(`from "@modelcontextprotocol/sdk-shared"`); + expect(result).toContain('JSONRPCErrorSchema'); + expect(result).not.toContain('@modelcontextprotocol/sdk/types'); + }); + + it('emits a split diagnostic for a re-export mixing a spec schema and a *Schema type (no silent breakage)', () => { + // The `*Schema` suffix would have routed BooleanSchema to sdk-shared silently (no such export); + // membership routing instead surfaces the mismatch so the user splits the re-export manually. + const input = `export { CallToolResultSchema, BooleanSchema } from '@modelcontextprotocol/sdk/types.js';\n`; + const project = new Project({ useInMemoryFileSystem: true }); + const sourceFile = project.createSourceFile('test.ts', input); + const result = importPathsTransform.apply(sourceFile, { projectType: 'server' }); + expect(result.diagnostics.some(d => d.message.includes('mixes symbols') && d.message.includes('Split'))).toBe(true); + }); + it('flags *Schema accesses through a namespace import of sdk/types.js (cannot be split)', () => { const input = [ `import * as types from '@modelcontextprotocol/sdk/types.js';`, diff --git a/packages/sdk-shared/src/index.ts b/packages/sdk-shared/src/index.ts index 89ac033328..25745b3b9f 100644 --- a/packages/sdk-shared/src/index.ts +++ b/packages/sdk-shared/src/index.ts @@ -11,14 +11,15 @@ // Scope: Zod schemas ONLY. The corresponding spec TypeScript types, error classes, enums, and // type guards are part of the public API of @modelcontextprotocol/server and /client. // -// The list below is the complete set of `export const *Schema` declarations in core's schema -// module; the sdkSharedSchemas test asserts it stays in sync. The @modelcontextprotocol/core -// specifier is aliased (tsconfig.json + tsdown.config.ts) to core's schemas module and bundled. +// The list below is the spec `*Schema` set: every `export const *Schema` in core's schema module +// EXCEPT internal helper schemas that have no public spec type (e.g. BaseRequestParamsSchema, +// NotificationsParamsSchema). It mirrors core's SPEC_SCHEMA_KEYS allowlist; the sdkSharedSchemas +// test asserts it stays in sync. The @modelcontextprotocol/core specifier is aliased (tsconfig.json +// + tsdown.config.ts) to core's schemas module and bundled. export { AnnotationsSchema, AudioContentSchema, BaseMetadataSchema, - BaseRequestParamsSchema, BlobResourceContentsSchema, BooleanSchemaSchema, CallToolRequestParamsSchema, @@ -32,7 +33,6 @@ export { ClientNotificationSchema, ClientRequestSchema, ClientResultSchema, - ClientTasksCapabilitySchema, CompatibilityCallToolResultSchema, CompleteRequestParamsSchema, CompleteRequestSchema, @@ -81,7 +81,6 @@ export { JSONRPCResultResponseSchema, JSONValueSchema, LegacyTitledEnumSchemaSchema, - ListChangedOptionsBaseSchema, ListPromptsRequestSchema, ListPromptsResultSchema, ListResourcesRequestSchema, @@ -101,7 +100,6 @@ export { ModelPreferencesSchema, MultiSelectEnumSchemaSchema, NotificationSchema, - NotificationsParamsSchema, NumberSchemaSchema, PaginatedRequestParamsSchema, PaginatedRequestSchema, @@ -145,7 +143,6 @@ export { ServerNotificationSchema, ServerRequestSchema, ServerResultSchema, - ServerTasksCapabilitySchema, SetLevelRequestParamsSchema, SetLevelRequestSchema, SingleSelectEnumSchemaSchema, diff --git a/packages/sdk-shared/test/sdkSharedSchemas.test.ts b/packages/sdk-shared/test/sdkSharedSchemas.test.ts index aba0852f7c..7dc4e93e4d 100644 --- a/packages/sdk-shared/test/sdkSharedSchemas.test.ts +++ b/packages/sdk-shared/test/sdkSharedSchemas.test.ts @@ -14,15 +14,28 @@ describe('@modelcontextprotocol/sdk-shared', () => { expect(InitializeRequestSchema.safeParse({}).success).toBe(false); }); - it('re-exports every *Schema declared in core (drift guard)', () => { - // If core gains a new spec schema, this fails until it is added to src/index.ts. - // (Renames/removals are already caught by typecheck — the named re-export would not resolve.) + it('re-exports exactly the spec schemas declared in core — no internal helpers (drift guard)', () => { + // sdk-shared's public surface is the spec `*Schema` constants ONLY. Some `*Schema` consts in + // core's schemas.ts are internal building blocks with no public spec type; they must NOT leak + // here. This list mirrors the exclusion in core's specTypeSchema.ts (SPEC_SCHEMA_KEYS) — keep + // the two in sync. + const INTERNAL_HELPER_SCHEMAS = [ + 'BaseRequestParamsSchema', + 'ClientTasksCapabilitySchema', + 'ListChangedOptionsBaseSchema', + 'NotificationsParamsSchema', + 'ServerTasksCapabilitySchema' + ]; const src = readFileSync(fileURLToPath(new URL('../../core/src/types/schemas.ts', import.meta.url)), 'utf8'); const coreSchemas = [...src.matchAll(/^export const (\w+Schema)\b/gm)] .map(m => m[1]) - .filter((name): name is string => name !== undefined); - const exported = new Set(Object.keys(sdkShared)); - expect(coreSchemas.filter(name => !exported.has(name))).toEqual([]); - expect(coreSchemas.length).toBeGreaterThanOrEqual(159); + .filter((name): name is string => name !== undefined && /^[A-Z]/.test(name)); + // The spec schema set = every PascalCase core `*Schema` const minus the internal helpers. + const specSchemas = coreSchemas.filter(name => !INTERNAL_HELPER_SCHEMAS.includes(name)).sort(); + const exported = Object.keys(sdkShared).sort(); + // Exact match, both directions: a new core spec schema missing here fails (we forgot to + // re-export it), and any internal helper / non-spec symbol that leaks here also fails. + expect(exported).toEqual(specSchemas); + expect(specSchemas.length).toBeGreaterThanOrEqual(154); }); }); From 10a4549e327dc3198185140fbbe7b5f4c7d6ca7f Mon Sep 17 00:00:00 2001 From: Konstantin Konstantinov Date: Thu, 25 Jun 2026 08:59:28 +0300 Subject: [PATCH 07/21] clean up --- ...5-05-21-codemod-findreferences-refactor.md | 957 ------------------ .../2026-05-15-codemod-batch-test-fixes.md | 549 ---------- .../plans/2026-06-02-readbuffer-max-size.md | 323 ------ .../2026-06-02-v1-readbuffer-max-size.md | 356 ------- .../plans/2026-06-23-sdk-shared-package.md | 716 ------------- .../2026-05-11-codemod-batch-test-design.md | 288 ------ .../2026-06-02-readbuffer-max-size-design.md | 61 -- .../specs/2026-06-08-sep-2549-ttl-design.md | 495 --------- .../2026-06-23-sdk-shared-package-design.md | 135 --- 9 files changed, 3880 deletions(-) delete mode 100644 docs/superpowers/plans/2025-05-21-codemod-findreferences-refactor.md delete mode 100644 docs/superpowers/plans/2026-05-15-codemod-batch-test-fixes.md delete mode 100644 docs/superpowers/plans/2026-06-02-readbuffer-max-size.md delete mode 100644 docs/superpowers/plans/2026-06-02-v1-readbuffer-max-size.md delete mode 100644 docs/superpowers/plans/2026-06-23-sdk-shared-package.md delete mode 100644 docs/superpowers/specs/2026-05-11-codemod-batch-test-design.md delete mode 100644 docs/superpowers/specs/2026-06-02-readbuffer-max-size-design.md delete mode 100644 docs/superpowers/specs/2026-06-08-sep-2549-ttl-design.md delete mode 100644 docs/superpowers/specs/2026-06-23-sdk-shared-package-design.md diff --git a/docs/superpowers/plans/2025-05-21-codemod-findreferences-refactor.md b/docs/superpowers/plans/2025-05-21-codemod-findreferences-refactor.md deleted file mode 100644 index d907040b24..0000000000 --- a/docs/superpowers/plans/2025-05-21-codemod-findreferences-refactor.md +++ /dev/null @@ -1,957 +0,0 @@ -# Codemod: Replace Manual AST Walking with `findReferencesAsNodes()` - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Simplify codemod transforms by replacing manual `forEachDescendant` + parent-kind-guard patterns with ts-morph's `findReferencesAsNodes()`, eliminating ~12 parent-kind guards, ~4 duplicate scope checks, and ~5 manual AST walk functions. - -**Architecture:** ts-morph's TypeScript language service already resolves symbol bindings in the current syntax-only Project mode (no tsconfig needed). `findReferencesAsNodes()` returns precisely the references to a given symbol — correctly scoped, excluding property-name positions, and handling aliases. We refactor transforms to collect references via this API *before* mutating the AST, then apply changes in reverse-position order (a pattern the codemod already uses). A second phase optionally loads the user's tsconfig for receiver-type checking. - -**Tech Stack:** ts-morph v28, vitest - -**Key invariant:** `findReferencesAsNodes()` must be called *before* the symbol binding is modified (e.g., before an import specifier is renamed or removed). After mutation, collected Node objects remain valid but the language service can no longer resolve the original binding. - ---- - -## File Map - -| File | Action | Responsibility | -|------|--------|----------------| -| `packages/codemod/src/utils/astUtils.ts` | Modify | Replace `renameAllReferences` internals with `findReferencesAsNodes()` | -| `packages/codemod/src/utils/importUtils.ts` | Modify | Add `findImportSpecifierByName`, simplify `removeUnusedImport` | -| `packages/codemod/src/migrations/v1-to-v2/transforms/symbolRenames.ts` | Modify | Collect refs before import mutation; use `findReferencesAsNodes()` in ErrorCode/RHE handlers | -| `packages/codemod/src/migrations/v1-to-v2/transforms/contextTypes.ts` | Modify | Use `findReferencesAsNodes()` on `extra` param; eliminate parent-kind guards and manual scope check | -| `packages/codemod/src/migrations/v1-to-v2/transforms/specSchemaAccess.ts` | Modify | Use `findReferencesAsNodes()` for schema refs; eliminate `findNonImportReferences()` | -| `packages/codemod/src/migrations/v1-to-v2/transforms/importPaths.ts` | Modify | Collect refs before import removal for renamed symbols | -| `packages/codemod/src/migrations/v1-to-v2/transforms/removedApis.ts` | Modify | Collect refs before import removal | -| `packages/codemod/src/types.ts` | Modify | Add optional `project` to `TransformContext` (Phase 2) | -| `packages/codemod/src/runner.ts` | Modify | Optionally resolve tsconfig; pass Project via context (Phase 2) | -| `packages/codemod/src/utils/projectAnalyzer.ts` | Modify | Add `findTsConfig()` (Phase 2) | -| All test files under `packages/codemod/test/v1-to-v2/transforms/` | Verify | Existing tests must pass unchanged — this is a refactor under green | - ---- - -## Phase 1: `findReferencesAsNodes()` Refactor (no tsconfig needed) - -### Task 1: Rewrite `renameAllReferences` in astUtils.ts - -The current function (33 lines, 12 parent-kind guards) manually walks all identifiers and filters by parent kind. `findReferencesAsNodes()` eliminates 10 of those 12 guards — only `ShorthandPropertyAssignment` and `ExportSpecifier` need special handling since they require AST expansion (not just text replacement). - -**Files:** -- Modify: `packages/codemod/src/utils/astUtils.ts` -- Verify: `packages/codemod/test/v1-to-v2/transforms/symbolRenames.test.ts` (primary consumer) - -- [ ] **Step 1: Read the current implementation** - -Read `packages/codemod/src/utils/astUtils.ts` — the entire file is the `renameAllReferences` function. - -Current implementation walks all identifiers with matching text and checks 12 parent kinds: -``` -ImportSpecifier, ExportSpecifier, PropertyAssignment (name), PropertyAccessExpression (name), -PropertySignature (name), MethodDeclaration (name), MethodSignature (name), -PropertyDeclaration (name), EnumMember (name), BindingElement (propertyName), -GetAccessorDeclaration (name), SetAccessorDeclaration (name), ShorthandPropertyAssignment -``` - -- [ ] **Step 2: Rewrite using `findReferencesAsNodes()`** - -Replace the body of `renameAllReferences` with: - -```typescript -import type { SourceFile } from 'ts-morph'; -import { Node } from 'ts-morph'; - -export function renameAllReferences(sourceFile: SourceFile, oldName: string, newName: string): void { - // Find the first identifier with this name to use as the findReferences anchor. - // Must be called BEFORE the symbol's import specifier is renamed/removed. - let anchor: import('ts-morph').Node | undefined; - sourceFile.forEachDescendant(node => { - if (anchor) return; - if (Node.isIdentifier(node) && node.getText() === oldName) { - anchor = node; - } - }); - if (!anchor) return; - - const refs = anchor.findReferencesAsNodes(); - - // Apply in reverse position order to avoid invalidating earlier nodes - const sorted = refs.toSorted((a, b) => b.getStart() - a.getStart()); - for (const ref of sorted) { - if (ref.wasForgotten()) continue; - const parent = ref.getParent(); - if (!parent) continue; - - // Skip import specifiers — caller manages those - if (Node.isImportSpecifier(parent)) continue; - - // ExportSpecifier: preserve public name by adding alias - if (Node.isExportSpecifier(parent)) { - if (parent.getAliasNode() === ref) continue; - if (!parent.getAliasNode()) parent.setAlias(oldName); - parent.getNameNode().replaceWithText(newName); - continue; - } - - // ShorthandPropertyAssignment: expand { McpError } → { McpError: ProtocolError } - if (Node.isShorthandPropertyAssignment(parent)) { - parent.replaceWithText(`${oldName}: ${newName}`); - continue; - } - - ref.replaceWithText(newName); - } -} -``` - -The 10 parent-kind guards (PropertyAssignment name, PropertyAccessExpression name, PropertySignature name, MethodDeclaration name, MethodSignature name, PropertyDeclaration name, EnumMember name, BindingElement propertyName, GetAccessor name, SetAccessor name) are all handled automatically by `findReferencesAsNodes()` — it never returns identifier nodes in property-name positions. - -- [ ] **Step 3: Run all transform tests to verify** - -Run: `pnpm --filter @modelcontextprotocol/codemod test` - -Expected: all tests pass. The `renameAllReferences` function is called by `symbolRenames`, `importPaths`, and `removedApis` transforms — all their tests exercise it. - -- [ ] **Step 4: Suggest commit** - -``` -feat(codemod): rewrite renameAllReferences using findReferencesAsNodes - -Replace manual 12-case parent-kind guard with ts-morph's -findReferencesAsNodes() which handles scope and position -classification automatically. Only ShorthandPropertyAssignment -and ExportSpecifier need explicit handling for AST expansion. -``` - ---- - -### Task 2: Refactor `symbolRenames.ts` — collect refs before import mutation - -The SIMPLE_RENAMES loop currently modifies the import specifier first, then calls `renameAllReferences`. But `findReferencesAsNodes()` must be called *before* the binding is modified. This task reorders the operations. - -The three `forEachDescendant` walks in `handleErrorCodeSplit` and `handleRequestHandlerExtra` are also replaced. - -**Files:** -- Modify: `packages/codemod/src/migrations/v1-to-v2/transforms/symbolRenames.ts` -- Verify: `packages/codemod/test/v1-to-v2/transforms/symbolRenames.test.ts` - -- [ ] **Step 1: Read current file** - -Read `packages/codemod/src/migrations/v1-to-v2/transforms/symbolRenames.ts` (352 lines). - -The SIMPLE_RENAMES loop (lines 23-37): -```typescript -for (const namedImport of imp.getNamedImports()) { - const name = namedImport.getName(); - const newName = SIMPLE_RENAMES[name]; - if (newName) { - namedImport.setName(newName); // modifies binding FIRST - const alias = namedImport.getAliasNode(); - if (!alias) { - renameAllReferences(sourceFile, name, newName); // then renames body - } - changesCount++; - } -} -``` - -- [ ] **Step 2: Reorder SIMPLE_RENAMES to collect-before-mutate** - -```typescript -for (const namedImport of imp.getNamedImports()) { - const name = namedImport.getName(); - const newName = SIMPLE_RENAMES[name]; - if (newName) { - const alias = namedImport.getAliasNode(); - if (!alias) { - // Collect refs while binding is still intact - renameAllReferences(sourceFile, name, newName); - } - namedImport.setName(newName); // modify binding AFTER refs are renamed - changesCount++; - } -} -``` - -Note: this is just reordering the two operations. `renameAllReferences` (from Task 1) now uses `findReferencesAsNodes()` internally, which requires the binding to still exist. Moving `setName` after `renameAllReferences` satisfies this requirement. - -- [ ] **Step 3: Refactor `handleErrorCodeSplit` to use `findReferencesAsNodes()`** - -Current code (lines 71-85) does a manual `forEachDescendant` looking for `Node.isPropertyAccessExpression` where the expression is `ErrorCode`. Replace with: - -```typescript -function handleErrorCodeSplit(sourceFile: SourceFile, diagnostics: Diagnostic[]): number { - let changesCount = 0; - - const imports = sourceFile.getImportDeclarations(); - let errorCodeImport: ReturnType<(typeof imports)[0]['getNamedImports']>[0] | undefined; - - for (const imp of imports) { - if (!isAnyMcpSpecifier(imp.getModuleSpecifierValue())) continue; - for (const namedImport of imp.getNamedImports()) { - if (namedImport.getName() === 'ErrorCode') { - errorCodeImport = namedImport; - break; - } - } - if (errorCodeImport) break; - } - - if (!errorCodeImport) return 0; - - // Collect ALL references while binding exists - const refs = errorCodeImport.getNameNode().findReferencesAsNodes() - .filter(n => !Node.isImportSpecifier(n.getParent())); - - let needsProtocolErrorCode = false; - let needsSdkErrorCode = false; - - // Classify each reference - const replacements: { node: import('ts-morph').Node; newText: string }[] = []; - for (const ref of refs) { - const parent = ref.getParent(); - if (!parent || !Node.isPropertyAccessExpression(parent)) continue; - if (parent.getExpression() !== ref) continue; - - const member = parent.getName(); - if (ERROR_CODE_SDK_MEMBERS.has(member)) { - needsSdkErrorCode = true; - replacements.push({ node: ref, newText: 'SdkErrorCode' }); - } else { - needsProtocolErrorCode = true; - replacements.push({ node: ref, newText: 'ProtocolErrorCode' }); - } - changesCount++; - } - - // Apply replacements in reverse order - const sorted = replacements.toSorted((a, b) => b.node.getStart() - a.node.getStart()); - for (const { node, newText } of sorted) { - node.replaceWithText(newText); - } - - // ... rest of import cleanup (unchanged from current code, lines 87-143) ... -``` - -This eliminates the `forEachDescendant` walk. The `errorCodeLocalName` variable and manual alias handling are also gone — `findReferencesAsNodes()` resolves aliases automatically. - -- [ ] **Step 4: Refactor `handleRequestHandlerExtra` similarly** - -The `forEachDescendant` walk at line 189 that finds `Node.isTypeReference` matching `extraLocalName` becomes: - -```typescript -// Collect refs while binding exists -const refs = extraImport.getNameNode().findReferencesAsNodes() - .filter(n => !Node.isImportSpecifier(n.getParent())); -``` - -The rest of the classification logic (checking `ServerRequest`/`ClientNotification` type args) stays the same — it operates on the parent `TypeReference` node. But we no longer need `extraLocalName` or manual alias handling. - -- [ ] **Step 5: Run tests** - -Run: `pnpm --filter @modelcontextprotocol/codemod test -- test/v1-to-v2/transforms/symbolRenames.test.ts` - -Expected: all tests pass, including alias tests (lines 366-399). - -- [ ] **Step 6: Suggest commit** - -``` -refactor(codemod): use findReferencesAsNodes in symbolRenames - -Collect symbol references via findReferencesAsNodes() before -mutating import specifiers. Eliminates three forEachDescendant -walks and manual alias tracking in handleErrorCodeSplit and -handleRequestHandlerExtra. -``` - ---- - -### Task 3: Refactor `contextTypes.ts` — eliminate parent-kind guards and scope checks - -This transform has the second-highest complexity. The `processCallback` function (lines 18-177): -- Walks callback body with `forEachDescendant` looking for `extra` identifiers (line 98) -- Checks 4 parent kinds to exclude property-name positions (lines 102-105) -- Does a separate `forEachDescendant` walk to check for `ctx` name conflicts (lines 63-74) -- Builds replacements with property mappings (lines 111-134) - -All of this collapses with `findReferencesAsNodes()` on the parameter. - -**Files:** -- Modify: `packages/codemod/src/migrations/v1-to-v2/transforms/contextTypes.ts` -- Verify: `packages/codemod/test/v1-to-v2/transforms/contextTypes.test.ts` - -- [ ] **Step 1: Read the current processCallback function** - -Read `packages/codemod/src/migrations/v1-to-v2/transforms/contextTypes.ts:18-177`. - -Key sections to replace: -- Lines 61-84: scope-conflict check (walk looking for `ctx` identifier) -- Lines 96-107: collect identifiers matching `extra`, filter 4 parent kinds -- Lines 110-135: build replacements - -- [ ] **Step 2: Replace identifier collection with findReferencesAsNodes** - -Replace lines 62-84 (scope conflict check) and lines 96-107 (identifier collection) with: - -```typescript - // Check for ctx name conflicts in the callback body using findReferences on - // any existing 'ctx' identifier — if found, it means ctx is in scope. - if (body) { - let ctxAlreadyInScope = false; - body.forEachDescendant(node => { - if (ctxAlreadyInScope) return; - if (Node.isIdentifier(node) && node.getText() === CTX_PARAM_NAME) { - // Check it's not inside a nested function that shadows it - const containingFn = node.getFirstAncestor(n => - Node.isArrowFunction(n) || Node.isFunctionExpression(n) || Node.isFunctionDeclaration(n) - ); - if (containingFn === callbackNode || !containingFn) { - ctxAlreadyInScope = true; - } - } - }); - if (ctxAlreadyInScope) { - diagnostics.push( - warning( - sourceFile.getFilePath(), - extraParam.getStartLineNumber(), - `Cannot rename '${EXTRA_PARAM_NAME}' to '${CTX_PARAM_NAME}': '${CTX_PARAM_NAME}' is already referenced in this scope. Manual migration required.` - ) - ); - return -1; - } - } - - // Collect references to the 'extra' parameter using findReferencesAsNodes. - // This automatically: - // - scopes to this specific parameter binding (ignores shadowed 'extra' in nested fns) - // - excludes property-name positions ({ extra: value }, obj.extra, etc.) - const paramRefs = extraParam.getNameNode().findReferencesAsNodes() - .filter(n => !Node.isParameter(n.getParent())); - - // Rename param declaration - const paramDecl = extraParam.getNameNode(); - paramDecl.replaceWithText(CTX_PARAM_NAME); - - // Build replacements from collected references - const sortedMappings = [...CONTEXT_PROPERTY_MAP] - .filter(m => m.from !== m.to) - .toSorted((a, b) => b.from.length - a.from.length); - - const replacements: { node: import('ts-morph').Node; newText: string }[] = []; - for (const ref of paramRefs) { - const parent = ref.getParent(); - // Value-position property access: extra.signal → ctx.mcpReq.signal - if (parent && Node.isPropertyAccessExpression(parent) && parent.getExpression() === ref) { - const propName = '.' + parent.getName(); - const mapping = sortedMappings.find(m => m.from === propName); - if (mapping) { - replacements.push({ node: parent, newText: CTX_PARAM_NAME + mapping.to }); - continue; - } - } - // Type-position qualified name: typeof extra.signal → typeof ctx.mcpReq.signal - if (parent && parent.getKind() === SyntaxKind.QualifiedName && parent.getChildAtIndex(0) === ref) { - const right = parent.getChildAtIndex(2); - if (right) { - const propName = '.' + right.getText(); - const mapping = sortedMappings.find(m => m.from === propName); - if (mapping) { - replacements.push({ node: parent, newText: CTX_PARAM_NAME + mapping.to }); - continue; - } - } - } - replacements.push({ node: ref, newText: CTX_PARAM_NAME }); - } - - const sorted = replacements.toSorted((a, b) => b.node.getStart() - a.node.getStart()); - for (const { node, newText } of sorted) { - node.replaceWithText(newText); - } -``` - -**What's eliminated:** -- The 4-case parent-kind exclusion list (lines 102-106) — `findReferencesAsNodes()` handles these -- The nested-function-aware scope walk for conflict detection (lines 63-74) — simplified to a targeted check - -**What stays the same:** -- Property mapping logic (PropertyAccessExpression / QualifiedName) — this is transform-specific -- The outer call-finding loop and callback detection -- The post-rewrite destructuring warning - -- [ ] **Step 3: Run tests** - -Run: `pnpm --filter @modelcontextprotocol/codemod test -- test/v1-to-v2/transforms/contextTypes.test.ts` - -Expected: all tests pass. Key tests to watch: -- `should not rename 'extra' in property positions` (verifies parent-kind exclusion) -- `should not rename when ctx already exists` (verifies scope conflict) -- `should handle nested functions` (verifies scope isolation) - -- [ ] **Step 4: Suggest commit** - -``` -refactor(codemod): use findReferencesAsNodes in contextTypes - -Replace manual forEachDescendant + 4-case parent-kind guard with -findReferencesAsNodes() on the 'extra' parameter. The language -service handles scope isolation and property-name exclusion -automatically. -``` - ---- - -### Task 4: Refactor `specSchemaAccess.ts` — eliminate `findNonImportReferences` and scoped walks - -This is the most complex transform (350 lines, 6 parent-kind guards, 3-level parent walks). Two `forEachDescendant` walks are replaced. - -**Files:** -- Modify: `packages/codemod/src/migrations/v1-to-v2/transforms/specSchemaAccess.ts` -- Verify: `packages/codemod/test/v1-to-v2/transforms/specSchemaAccess.test.ts` - -- [ ] **Step 1: Read the current file** - -Read `packages/codemod/src/migrations/v1-to-v2/transforms/specSchemaAccess.ts`. - -Key sections: -- `findNonImportReferences` (lines 51-61): manual forEachDescendant walk -- `handleReference` (lines 63-192): 6 parent-kind guards at lines 129, 143, 154, 168, 172, 176 -- `rewriteCapturedSafeParse` (lines 249-335): scoped forEachDescendant walk at line 269 - -- [ ] **Step 2: Replace `findNonImportReferences` with `findReferencesAsNodes`** - -In the main loop (lines 19-31), replace: -```typescript -const refs = findNonImportReferences(sourceFile, localName); -``` -with: -```typescript -// Find the import specifier node for this schema -const specNode = schemaImports.get(localName)!.specifier; -const refs = specNode.getNameNode().findReferencesAsNodes() - .filter(n => !Node.isImportSpecifier(n.getParent())); -``` - -This requires changing `collectSpecSchemaImports` to also return the specifier node: -```typescript -function collectSpecSchemaImports(sourceFile: SourceFile): Map { - const result = new Map(); - for (const imp of sourceFile.getImportDeclarations()) { - if (!isAnyMcpSpecifier(imp.getModuleSpecifierValue())) continue; - for (const n of imp.getNamedImports()) { - const exportName = n.getName(); - if (!SPEC_SCHEMA_NAMES.has(exportName)) continue; - const localName = n.getAliasNode()?.getText() ?? exportName; - result.set(localName, { originalName: exportName, specifier: n }); - } - } - return result; -} -``` - -Delete the `findNonImportReferences` function entirely. - -- [ ] **Step 3: Simplify `handleReference` parent-kind guards** - -With `findReferencesAsNodes()`, we no longer get identifiers in property-name positions. Remove these now-unreachable guards: - -```typescript -// REMOVE — findReferencesAsNodes never returns property-name-position identifiers: -// - line 168: Node.isPropertyAssignment(parent) && parent.getNameNode() === ref -// - line 172: Node.isBindingElement(parent) && parent.getPropertyNameNode() === ref -// - line 176: Node.isPropertyAccessExpression(parent) && parent.getNameNode() === ref -``` - -Keep these — they classify the reference type, not exclude positions: -- `isTypeofInTypePosition` — distinguishes type-level `typeof X` from value usage -- `isSafeParseSuccessPattern` / `isSafeParsePattern` / `isParsePattern` — detect Zod API patterns -- `Node.isPropertyAccessExpression(parent) && parent.getExpression() === ref` — value-position property access -- `Node.isExportSpecifier(parent)` — re-export position -- `Node.isShorthandPropertyAssignment(parent)` — shorthand property - -- [ ] **Step 4: Replace scoped walk in `rewriteCapturedSafeParse`** - -Current code (lines 268-317) does `scope.forEachDescendant` to find `${varName}.success`, `${varName}.data`, `${varName}.error` accesses. Replace with `findReferencesAsNodes()` on the variable declaration: - -```typescript -function rewriteCapturedSafeParse( - safeParseCall: import('ts-morph').CallExpression, - localName: string, - typeName: string, - sourceFile: SourceFile, - diagnostics: Diagnostic[] -): boolean { - const varDecl = safeParseCall.getParent() as import('ts-morph').VariableDeclaration; - const varName = varDecl.getName(); - const args = safeParseCall.getArguments(); - const argText = args.length > 0 ? args[0]!.getText() : ''; - - // Collect references to the result variable BEFORE rewriting the initializer - const varNameNode = varDecl.getNameNode(); - const varRefs = varNameNode.findReferencesAsNodes() - .filter(n => n !== varNameNode && !Node.isVariableDeclaration(n.getParent())); - - // Rewrite the safeParse call - safeParseCall.replaceWithText(`specTypeSchemas.${typeName}['~standard'].validate(${argText})`); - ensureImport(sourceFile, 'specTypeSchemas'); - - // Classify property accesses on the result variable - const replacements: { node: import('ts-morph').Node; newText: string }[] = []; - for (const ref of varRefs) { - const parent = ref.getParent(); - if (!parent || !Node.isPropertyAccessExpression(parent)) continue; - if (parent.getExpression() !== ref) continue; - - const propName = parent.getName(); - switch (propName) { - case 'success': { - const grandParent = parent.getParent(); - if (grandParent && Node.isPrefixUnaryExpression(grandParent) && - grandParent.getOperatorToken() === SyntaxKind.ExclamationToken) { - replacements.push({ node: grandParent, newText: `${varName}.issues !== undefined` }); - } else { - replacements.push({ node: parent, newText: `(${varName}.issues === undefined)` }); - } - break; - } - case 'data': - replacements.push({ node: parent, newText: `${varName}.value` }); - break; - case 'error': { - const errorParent = parent.getParent(); - if (errorParent && Node.isPropertyAccessExpression(errorParent) && errorParent.getExpression() === parent) { - const subProp = errorParent.getName(); - if (subProp === 'issues') { - replacements.push({ node: errorParent, newText: `${varName}.issues` }); - } else if (subProp === 'message') { - replacements.push({ node: errorParent, newText: `${varName}.issues?.map(i => i.message).join(', ')` }); - } else { - diagnostics.push(warning(sourceFile.getFilePath(), errorParent.getStartLineNumber(), - `${varName}.error.${subProp} has no StandardSchema equivalent. Manual migration required.`)); - } - } else { - replacements.push({ node: parent, newText: `${varName}.issues` }); - } - break; - } - } - } - - const sorted = replacements.toSorted((a, b) => b.node.getStart() - a.node.getStart()); - for (const { node, newText } of sorted) { - node.replaceWithText(newText); - } - - diagnostics.push(warning(sourceFile.getFilePath(), varDecl.getStartLineNumber(), - `Rewrote ${localName}.safeParse() to specTypeSchemas.${typeName}['~standard'].validate(). ` + - `Result properties remapped: .success → .issues === undefined, .data → .value, .error → .issues.`)); - - return true; -} -``` - -**What's eliminated:** -- `findNonImportReferences` function (11 lines) — deleted entirely -- 3 unreachable parent-kind guards in `handleReference` -- The `scope.forEachDescendant` walk in `rewriteCapturedSafeParse` (was scope-insensitive anyway, as a PR comment noted) - -- [ ] **Step 5: Run tests** - -Run: `pnpm --filter @modelcontextprotocol/codemod test -- test/v1-to-v2/transforms/specSchemaAccess.test.ts` - -Expected: all tests pass. Key tests: -- Aliased import `import { CallToolRequestSchema as CTRS }` (line 493) -- Captured safeParse rewrite (line 248+) -- Non-MCP schemas not touched (line 222+) - -- [ ] **Step 6: Suggest commit** - -``` -refactor(codemod): use findReferencesAsNodes in specSchemaAccess - -Delete findNonImportReferences() and replace both forEachDescendant -walks with findReferencesAsNodes(). The scoped safeParse-result -rewrite now uses findReferencesAsNodes on the variable declaration, -which is inherently scope-correct. -``` - ---- - -### Task 5: Refactor `importPaths.ts` — collect refs before import removal - -Currently, `importPaths.ts` removes the old import (line 170), then calls `renameAllReferences` (line 172). Since Task 1's `renameAllReferences` now uses `findReferencesAsNodes()`, the binding must exist when it's called. Reorder operations. - -**Files:** -- Modify: `packages/codemod/src/migrations/v1-to-v2/transforms/importPaths.ts` -- Verify: `packages/codemod/test/v1-to-v2/transforms/importPaths.test.ts` - -- [ ] **Step 1: Read the relevant section** - -Read `packages/codemod/src/migrations/v1-to-v2/transforms/importPaths.ts:106-175`. - -The issue is at lines 162-175: -```typescript -for (const n of namedImports) { - // ... add pending imports ... -} -imp.remove(); // ← removes binding -changesCount++; -for (const [oldName, newName] of symbolsToRenameInFile) { - renameAllReferences(sourceFile, oldName, newName); // ← needs binding -} -``` - -- [ ] **Step 2: Move rename before import removal** - -```typescript -// Rename body references BEFORE removing the import (findReferencesAsNodes needs the binding) -for (const [oldName, newName] of symbolsToRenameInFile) { - renameAllReferences(sourceFile, oldName, newName); -} - -for (const n of namedImports) { - const name = n.getName(); - const resolvedName = mapping.renamedSymbols?.[name] ?? name; - const specifierTypeOnly = typeOnly || n.isTypeOnly(); - const symbolTarget = mapping.symbolTargetOverrides?.[name] ?? targetPackage; - usedPackages.add(symbolTarget); - addPending(symbolTarget, [resolvedName], specifierTypeOnly); -} -imp.remove(); -changesCount++; -``` - -Also apply the same reorder to the in-place `setModuleSpecifier` branch (lines 106-159): move `renameAllReferences` calls (lines 156-158) before `imp.setModuleSpecifier` (line 136) — though `setModuleSpecifier` doesn't break bindings, it's cleaner to be consistent. - -- [ ] **Step 3: Run tests** - -Run: `pnpm --filter @modelcontextprotocol/codemod test -- test/v1-to-v2/transforms/importPaths.test.ts` - -Expected: all tests pass. - -- [ ] **Step 4: Suggest commit** - -``` -refactor(codemod): reorder importPaths to rename refs before import removal - -findReferencesAsNodes() (used by renameAllReferences) needs the -import binding to still exist. Move rename calls before imp.remove(). -``` - ---- - -### Task 6: Refactor `removedApis.ts` — same reorder pattern - -Same issue: `renameAllReferences` called after import removal. - -**Files:** -- Modify: `packages/codemod/src/migrations/v1-to-v2/transforms/removedApis.ts` -- Verify: `packages/codemod/test/v1-to-v2/transforms/removedApis.test.ts` - -- [ ] **Step 1: Read the relevant sections** - -Read `packages/codemod/src/migrations/v1-to-v2/transforms/removedApis.ts`. - -Find all places where `renameAllReferences` is called and check whether the import binding has already been removed/modified. - -- [ ] **Step 2: Move renames before import removal** - -Apply the same pattern as Task 5: collect or apply renames before the import specifier or declaration is removed. - -- [ ] **Step 3: Run tests** - -Run: `pnpm --filter @modelcontextprotocol/codemod test -- test/v1-to-v2/transforms/removedApis.test.ts` - -Expected: all tests pass. - -- [ ] **Step 4: Suggest commit** - -``` -refactor(codemod): reorder removedApis to rename refs before import removal -``` - ---- - -### Task 7: Simplify `removeUnusedImport` in importUtils.ts - -The `removeUnusedImport` function (lines 116-141) does a manual `forEachDescendant` walk to count references. Replace with `findReferencesAsNodes()`. - -**Files:** -- Modify: `packages/codemod/src/utils/importUtils.ts` -- Verify: `pnpm --filter @modelcontextprotocol/codemod test` - -- [ ] **Step 1: Read the current function** - -Read `packages/codemod/src/utils/importUtils.ts:116-141`. - -- [ ] **Step 2: Rewrite using findReferencesAsNodes** - -```typescript -export function removeUnusedImport(sourceFile: SourceFile, symbolName: string, onlyMcpImports?: boolean): void { - for (const imp of sourceFile.getImportDeclarations()) { - if (onlyMcpImports && !isAnyMcpSpecifier(imp.getModuleSpecifierValue())) continue; - for (const namedImport of imp.getNamedImports()) { - if ((namedImport.getAliasNode()?.getText() ?? namedImport.getName()) === symbolName) { - // Check if the symbol has any non-import references - const refs = namedImport.getNameNode().findReferencesAsNodes() - .filter(n => !Node.isImportSpecifier(n.getParent())); - if (refs.length === 0) { - namedImport.remove(); - if (imp.getNamedImports().length === 0 && !imp.getDefaultImport() && !imp.getNamespaceImport()) { - imp.remove(); - } - } - return; - } - } - } -} -``` - -This eliminates the manual reference-counting `forEachDescendant` walk. - -- [ ] **Step 3: Run all tests** - -Run: `pnpm --filter @modelcontextprotocol/codemod test` - -Expected: all tests pass. `removeUnusedImport` is called by `specSchemaAccess` and `symbolRenames`. - -- [ ] **Step 4: Suggest commit** - -``` -refactor(codemod): use findReferencesAsNodes in removeUnusedImport -``` - ---- - -### Task 8: Full test suite verification and cleanup - -- [ ] **Step 1: Run full test suite** - -Run: `pnpm --filter @modelcontextprotocol/codemod test` - -Expected: all 14 test files pass. - -- [ ] **Step 2: Run typecheck** - -Run: `pnpm --filter @modelcontextprotocol/codemod typecheck` - -Expected: no type errors. - -- [ ] **Step 3: Run lint** - -Run: `pnpm --filter @modelcontextprotocol/codemod lint` - -Expected: no lint errors. - -- [ ] **Step 4: Remove dead code** - -Check if these functions are still used: -- `findNonImportReferences` in specSchemaAccess.ts — should be deleted (Task 4) -- Any unused imports in modified files - -- [ ] **Step 5: Suggest commit** - -``` -chore(codemod): remove dead code after findReferencesAsNodes refactor -``` - ---- - -## Phase 2: Optional tsconfig Loading for Receiver Type Checking - -This phase is independent of Phase 1 and addresses a different class of PR comments: transforms that cannot verify the *receiver* of a method call (e.g., `.tool()` might be on any object, not just `McpServer`). - -### Task 9: Add tsconfig resolution to projectAnalyzer - -**Files:** -- Modify: `packages/codemod/src/utils/projectAnalyzer.ts` -- Modify: `packages/codemod/src/types.ts` -- Modify: `packages/codemod/src/runner.ts` -- Test: `packages/codemod/test/projectAnalyzer.test.ts` - -- [ ] **Step 1: Add `findTsConfig` to projectAnalyzer** - -```typescript -export function findTsConfig(startDir: string): string | undefined { - let dir = path.resolve(startDir); - const root = path.parse(dir).root; - while (true) { - const candidate = path.join(dir, 'tsconfig.json'); - if (existsSync(candidate)) return candidate; - if (dir === root) return undefined; - if (PROJECT_ROOT_MARKERS.some(m => existsSync(path.join(dir, m)))) return undefined; - dir = path.dirname(dir); - } -} -``` - -- [ ] **Step 2: Extend `TransformContext` with optional Project** - -In `packages/codemod/src/types.ts`: - -```typescript -import type { Project, SourceFile } from 'ts-morph'; - -export interface TransformContext { - projectType: 'client' | 'server' | 'both' | 'unknown'; - project?: Project; - hasTypeInfo?: boolean; -} -``` - -- [ ] **Step 3: Modify runner to optionally load tsconfig** - -In `packages/codemod/src/runner.ts`, change Project creation: - -```typescript -import { findTsConfig } from './utils/projectAnalyzer.js'; - -const tsConfigPath = findTsConfig(options.targetDir); -const project = new Project({ - tsConfigFilePath: tsConfigPath, - skipAddingFilesFromTsConfig: true, - compilerOptions: { - allowJs: true, - noEmit: true, - skipLibCheck: true, - ...(tsConfigPath ? {} : { strict: false }), - } -}); - -// ... existing file globbing ... - -const hasTypeInfo = !!tsConfigPath; -const context: TransformContext = { - ...analyzeProject(options.targetDir), - project, - hasTypeInfo, -}; -``` - -Note: `skipAddingFilesFromTsConfig: true` keeps the current behavior of globbing files ourselves. But with a tsconfig, ts-morph resolves module paths and loads declaration files from `node_modules`. - -- [ ] **Step 4: Test with and without tsconfig** - -The existing tests use `new Project({ useInMemoryFileSystem: true })` and pass `TransformContext` without a `project` field. They should continue to work because `project` and `hasTypeInfo` are optional. - -Add a targeted test in `packages/codemod/test/projectAnalyzer.test.ts`: - -```typescript -describe('findTsConfig', () => { - it('should find tsconfig.json in target directory', () => { - const dir = mkdtempSync(join(tmpdir(), 'codemod-')); - writeFileSync(join(dir, 'tsconfig.json'), '{}'); - expect(findTsConfig(dir)).toBe(join(dir, 'tsconfig.json')); - rmSync(dir, { recursive: true }); - }); - - it('should walk up to find tsconfig.json', () => { - const dir = mkdtempSync(join(tmpdir(), 'codemod-')); - const subDir = join(dir, 'src'); - mkdirSync(subDir); - writeFileSync(join(dir, 'tsconfig.json'), '{}'); - expect(findTsConfig(subDir)).toBe(join(dir, 'tsconfig.json')); - rmSync(dir, { recursive: true }); - }); - - it('should return undefined when no tsconfig exists', () => { - const dir = mkdtempSync(join(tmpdir(), 'codemod-')); - mkdirSync(join(dir, '.git')); - expect(findTsConfig(dir)).toBeUndefined(); - rmSync(dir, { recursive: true }); - }); -}); -``` - -- [ ] **Step 5: Suggest commit** - -``` -feat(codemod): optionally resolve tsconfig for type-aware transforms - -When a tsconfig.json is found near the target directory, the ts-morph -Project loads it for module resolution and type information. Transforms -can check context.hasTypeInfo to use type-aware APIs. Falls back to -syntax-only mode when no tsconfig is found. -``` - ---- - -### Task 10: Add receiver type checking to `mcpServerApi.ts` - -When type info is available, verify that `.tool()` / `.prompt()` / `.resource()` calls are on an `McpServer` instance. This addresses the PR comment about false positives on `someOtherObj.tool()`. - -**Files:** -- Modify: `packages/codemod/src/migrations/v1-to-v2/transforms/mcpServerApi.ts` -- Verify: `packages/codemod/test/v1-to-v2/transforms/mcpServerApi.test.ts` - -- [ ] **Step 1: Add a receiver-type guard helper** - -At the top of `mcpServerApi.ts`: - -```typescript -function isMcpServerReceiver(expr: import('ts-morph').PropertyAccessExpression, context: TransformContext): boolean { - if (!context.hasTypeInfo) return true; // permissive when no types - - try { - const receiverType = expr.getExpression().getType(); - const symbol = receiverType.getSymbol(); - if (!symbol) return true; // can't determine — be permissive - const name = symbol.getName(); - return name === 'McpServer'; - } catch { - return true; // type resolution failed — be permissive - } -} -``` - -- [ ] **Step 2: Guard the call collection loop** - -In the switch statement (lines 33-59), add the guard: - -```typescript -for (const call of calls) { - const expr = call.getExpression(); - if (!Node.isPropertyAccessExpression(expr)) continue; - if (!isMcpServerReceiver(expr, context)) continue; // ← NEW - const methodName = expr.getName(); - // ... rest of switch ... -} -``` - -Note: `_context` parameter in `apply()` must be renamed to `context` since it's now used. - -- [ ] **Step 3: Run tests** - -Run: `pnpm --filter @modelcontextprotocol/codemod test -- test/v1-to-v2/transforms/mcpServerApi.test.ts` - -Expected: all tests pass. Tests use in-memory projects without type info, so `isMcpServerReceiver` returns `true` (permissive mode). - -- [ ] **Step 4: Suggest commit** - -``` -feat(codemod): add receiver type checking for McpServer API migration - -When type info is available (tsconfig resolved), verify that .tool(), -.prompt(), .resource() calls are on McpServer instances. Falls back -to permissive mode when types unavailable. -``` - ---- - -## Summary of Changes - -| Metric | Before | After Phase 1 | After Phase 2 | -|--------|--------|---------------|---------------| -| `renameAllReferences` parent guards | 12 | 2 (ShorthandProp, ExportSpecifier) | 2 | -| `contextTypes` parent guards | 4 | 0 | 0 | -| `specSchemaAccess` parent guards | 6 | 3 (pattern classification only) | 3 | -| `forEachDescendant` walks across all transforms | ~12 | ~4 | ~4 | -| Manual import-provenance functions | 6 | 6 (unchanged) | 6 (could reduce further) | -| Receiver type checking | none | none | mcpServerApi | -| Lines in astUtils.ts | 33 | ~28 | ~28 | -| Lines in specSchemaAccess.ts | 350 | ~300 | ~300 | -| Lines in contextTypes.ts | 257 | ~200 | ~200 | -| Lines in symbolRenames.ts | 352 | ~310 | ~310 | - -Phase 1 (Tasks 1-8) is the high-value work. Phase 2 (Tasks 9-10) is additive improvement. diff --git a/docs/superpowers/plans/2026-05-15-codemod-batch-test-fixes.md b/docs/superpowers/plans/2026-05-15-codemod-batch-test-fixes.md deleted file mode 100644 index 48c9d8de26..0000000000 --- a/docs/superpowers/plans/2026-05-15-codemod-batch-test-fixes.md +++ /dev/null @@ -1,549 +0,0 @@ -# Codemod Batch Test Fixes Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Fix 5 codemod transform issues discovered by running the batch test against real-world repos (inspector + mcp-servers-fork). - -**Architecture:** Each fix targets a specific transform or mapping file within `packages/codemod/src/migrations/v1-to-v2/`. Fixes are ordered by dependency: Tasks 1 and 4 are independent; Tasks 2 and 3 both modify `specSchemaAccess.ts` so Task 2 must land first; Task 5 is independent. All tasks follow TDD. - -**Tech Stack:** TypeScript, ts-morph (AST manipulation), vitest - ---- - -## File Map - -| File | Action | Task(s) | -|------|--------|---------| -| `packages/codemod/src/migrations/v1-to-v2/mappings/schemaToMethodMap.ts` | Modify | 1 | -| `packages/codemod/test/v1-to-v2/transforms/handlerRegistration.test.ts` | Modify | 1 | -| `packages/codemod/src/migrations/v1-to-v2/transforms/specSchemaAccess.ts` | Modify | 2, 3 | -| `packages/codemod/test/v1-to-v2/transforms/specSchemaAccess.test.ts` | Modify | 2, 3 | -| `packages/codemod/src/migrations/v1-to-v2/mappings/importMap.ts` | Modify | 4, 5 | -| `packages/codemod/test/v1-to-v2/transforms/importPaths.test.ts` | Modify | 4, 5 | - ---- - -### Task 1: Complete handler registration schema-to-method mapping - -Add missing experimental/task request schemas and notification schemas to `schemaToMethodMap.ts` so the `handlerRegistration` transform auto-converts them to string method names instead of falling through to `specSchemaAccess` which incorrectly replaces them with `specTypeSchemas.X`. - -**Impact:** Fixes ~20 errors in inspector/client `useConnection.ts` (setRequestHandler + downstream param type inference). - -**Files:** -- Modify: `packages/codemod/src/migrations/v1-to-v2/mappings/schemaToMethodMap.ts` -- Test: `packages/codemod/test/v1-to-v2/transforms/handlerRegistration.test.ts` - -- [ ] **Step 1: Write failing tests for task request schemas** - -Add to `handlerRegistration.test.ts`: - -```typescript -it('replaces ListTasksRequestSchema with method string', () => { - const input = [ - `import { ListTasksRequestSchema } from '@modelcontextprotocol/sdk/types.js';`, - `client.setRequestHandler(ListTasksRequestSchema, async (request) => {`, - ` return { tasks: [] };`, - `});`, - '' - ].join('\n'); - const result = applyTransform(input); - expect(result).toContain("setRequestHandler('tasks/list'"); - expect(result).not.toContain('ListTasksRequestSchema'); -}); - -it('replaces GetTaskRequestSchema with method string', () => { - const input = [ - `import { GetTaskRequestSchema } from '@modelcontextprotocol/sdk/types.js';`, - `client.setRequestHandler(GetTaskRequestSchema, async (request) => {`, - ` return { taskId: '1', status: 'completed' };`, - `});`, - '' - ].join('\n'); - const result = applyTransform(input); - expect(result).toContain("setRequestHandler('tasks/get'"); - expect(result).not.toContain('GetTaskRequestSchema'); -}); - -it('replaces CancelTaskRequestSchema with method string', () => { - const input = [ - `import { CancelTaskRequestSchema } from '@modelcontextprotocol/sdk/types.js';`, - `client.setRequestHandler(CancelTaskRequestSchema, async (request) => {`, - ` return {};`, - `});`, - '' - ].join('\n'); - const result = applyTransform(input); - expect(result).toContain("setRequestHandler('tasks/cancel'"); - expect(result).not.toContain('CancelTaskRequestSchema'); -}); - -it('replaces GetTaskPayloadRequestSchema with method string', () => { - const input = [ - `import { GetTaskPayloadRequestSchema } from '@modelcontextprotocol/sdk/types.js';`, - `client.setRequestHandler(GetTaskPayloadRequestSchema, async (request) => {`, - ` return { content: [] };`, - `});`, - '' - ].join('\n'); - const result = applyTransform(input); - expect(result).toContain("setRequestHandler('tasks/result'"); - expect(result).not.toContain('GetTaskPayloadRequestSchema'); -}); - -it('replaces TaskStatusNotificationSchema with method string', () => { - const input = [ - `import { TaskStatusNotificationSchema } from '@modelcontextprotocol/sdk/types.js';`, - `client.setNotificationHandler(TaskStatusNotificationSchema, async () => {});`, - '' - ].join('\n'); - const result = applyTransform(input); - expect(result).toContain("setNotificationHandler('notifications/tasks/status'"); - expect(result).not.toContain('TaskStatusNotificationSchema'); -}); - -it('replaces ElicitationCompleteNotificationSchema with method string', () => { - const input = [ - `import { ElicitationCompleteNotificationSchema } from '@modelcontextprotocol/sdk/types.js';`, - `client.setNotificationHandler(ElicitationCompleteNotificationSchema, async () => {});`, - '' - ].join('\n'); - const result = applyTransform(input); - expect(result).toContain("setNotificationHandler('notifications/elicitation/complete'"); - expect(result).not.toContain('ElicitationCompleteNotificationSchema'); -}); -``` - -- [ ] **Step 2: Run tests to verify they fail** - -Run: `pnpm --filter @modelcontextprotocol/codemod test -- test/v1-to-v2/transforms/handlerRegistration.test.ts` -Expected: 6 new tests FAIL (schemas not in map, get "Custom method handler" diagnostic instead) - -- [ ] **Step 3: Add missing schemas to the mapping** - -In `packages/codemod/src/migrations/v1-to-v2/mappings/schemaToMethodMap.ts`, add entries to `SCHEMA_TO_METHOD`: - -```typescript -ListTasksRequestSchema: 'tasks/list', -GetTaskRequestSchema: 'tasks/get', -GetTaskPayloadRequestSchema: 'tasks/result', -CancelTaskRequestSchema: 'tasks/cancel', -``` - -And add entries to `NOTIFICATION_SCHEMA_TO_METHOD`: - -```typescript -TaskStatusNotificationSchema: 'notifications/tasks/status', -ElicitationCompleteNotificationSchema: 'notifications/elicitation/complete', -``` - -- [ ] **Step 4: Run tests to verify they pass** - -Run: `pnpm --filter @modelcontextprotocol/codemod test -- test/v1-to-v2/transforms/handlerRegistration.test.ts` -Expected: All tests PASS - -- [ ] **Step 5: Commit** - -```bash -git add packages/codemod/src/migrations/v1-to-v2/mappings/schemaToMethodMap.ts packages/codemod/test/v1-to-v2/transforms/handlerRegistration.test.ts -git commit -m "fix(codemod): add task and elicitation schemas to handler registration map" -``` - ---- - -### Task 2: Replace schema identifiers in generic property access positions - -Currently, when a spec schema like `OAuthTokensSchema` is used with a Zod-specific method (e.g., `.parseAsync()`, `.or()`, `.extend()`), the `specSchemaAccess` transform only emits a diagnostic but does NOT replace the identifier. This leaves the old schema name in imports, which breaks compilation since v2 packages don't export these schema symbols. - -**Fix:** In the generic property access case, replace the identifier with `specTypeSchemas.X` (even though the method call itself won't work). The diagnostic still tells the user what to do, but the import now resolves. - -**Impact:** Fixes ~12 "Module has no exported member 'XSchema'" errors across both repos. - -**Files:** -- Modify: `packages/codemod/src/migrations/v1-to-v2/transforms/specSchemaAccess.ts` -- Test: `packages/codemod/test/v1-to-v2/transforms/specSchemaAccess.test.ts` - -- [ ] **Step 1: Write failing tests for generic property access replacement** - -Add to `specSchemaAccess.test.ts` in a new `describe` block: - -```typescript -describe('auto-transform: generic property access → specTypeSchemas.X', () => { - it('replaces schema identifier in .parseAsync() call', () => { - const input = [ - `import { OAuthTokensSchema } from '@modelcontextprotocol/server';`, - `const tokens = await OAuthTokensSchema.parseAsync(data);`, - '' - ].join('\n'); - const { text, result } = applyTransform(input); - expect(text).toContain('specTypeSchemas.OAuthTokens.parseAsync(data)'); - expect(text).not.toMatch(/import\s*\{[^}]*OAuthTokensSchema[^}]*\}/); - expect(result.changesCount).toBeGreaterThan(0); - expect(result.diagnostics.length).toBeGreaterThan(0); - }); - - it('replaces schema identifier in .or() call', () => { - const input = [ - `import { ServerNotificationSchema } from '@modelcontextprotocol/server';`, - `const union = ServerNotificationSchema.or(otherSchema);`, - '' - ].join('\n'); - const { text, result } = applyTransform(input); - expect(text).toContain('specTypeSchemas.ServerNotification.or(otherSchema)'); - expect(text).not.toMatch(/import\s*\{[^}]*ServerNotificationSchema[^}]*\}/); - expect(result.changesCount).toBeGreaterThan(0); - }); - - it('replaces schema identifier in .extend() call', () => { - const input = [ - `import { ToolSchema } from '@modelcontextprotocol/server';`, - `const extended = ToolSchema.extend({ extra: z.string() });`, - '' - ].join('\n'); - const { text, result } = applyTransform(input); - expect(text).toContain('specTypeSchemas.Tool.extend'); - expect(result.changesCount).toBeGreaterThan(0); - }); - - it('adds specTypeSchemas import for generic property access', () => { - const input = [ - `import { OAuthTokensSchema } from '@modelcontextprotocol/server';`, - `const tokens = await OAuthTokensSchema.parseAsync(data);`, - '' - ].join('\n'); - const { text } = applyTransform(input); - expect(text).toMatch(/import.*specTypeSchemas.*from/); - }); -}); -``` - -- [ ] **Step 2: Run tests to verify they fail** - -Run: `pnpm --filter @modelcontextprotocol/codemod test -- test/v1-to-v2/transforms/specSchemaAccess.test.ts` -Expected: 4 new tests FAIL (generic property access only emits diagnostic, doesn't replace) - -- [ ] **Step 3: Modify the generic property access handler to also replace the identifier** - -In `packages/codemod/src/migrations/v1-to-v2/transforms/specSchemaAccess.ts`, find the generic property access handler in `handleReference()` (around line 129). Change: - -```typescript -// BEFORE (diagnostic-only, no replacement): -if (parent && Node.isPropertyAccessExpression(parent) && parent.getExpression() === ref) { - diagnostics.push( - warning( - sourceFile.getFilePath(), - ref.getStartLineNumber(), - `${localName} is not exported in v2. Use \`specTypeSchemas.${typeName}\` (typed as StandardSchemaV1) or \`isSpecType.${typeName}\` for validation.` - ) - ); - return false; -} -``` - -to: - -```typescript -// AFTER (replace identifier AND emit diagnostic): -if (parent && Node.isPropertyAccessExpression(parent) && parent.getExpression() === ref) { - const line = ref.getStartLineNumber(); - ref.replaceWithText(`specTypeSchemas.${typeName}`); - ensureImport(sourceFile, 'specTypeSchemas'); - diagnostics.push( - warning( - sourceFile.getFilePath(), - line, - `Replaced ${localName} with specTypeSchemas.${typeName}. Note: typed as StandardSchemaV1, not ZodType — Zod methods like .safeParse()/.parse()/.parseAsync() are not available. Manual rewrite required.` - ) - ); - return true; -} -``` - -- [ ] **Step 4: Run tests to verify they pass** - -Run: `pnpm --filter @modelcontextprotocol/codemod test -- test/v1-to-v2/transforms/specSchemaAccess.test.ts` -Expected: All tests PASS (including existing "keeps original schema import when some refs are diagnostic-only" test — verify this one still passes since the behavior changed) - -**Note:** The existing test at line 262 ("keeps original schema import when some refs are diagnostic-only") combines a `.safeParse().success` auto-transform with a `.parse()` diagnostic-only case. The `.parse()` case is separate from the generic property access case (it has its own handler returning `false`). This test should still pass because `.parse()` is handled before the generic property access check. - -- [ ] **Step 5: Commit** - -```bash -git add packages/codemod/src/migrations/v1-to-v2/transforms/specSchemaAccess.ts packages/codemod/test/v1-to-v2/transforms/specSchemaAccess.test.ts -git commit -m "fix(codemod): replace schema identifiers in generic property access positions" -``` - ---- - -### Task 3: Fix safeParse-to-validate `.error` sub-property remapping - -When `const r = XSchema.safeParse(v)` is captured, the transform rewrites `.error` → `.issues`. But downstream accesses like `r.error.message` become `r.issues.message` (wrong — `.issues` is an array) and `r.error.issues` becomes `r.issues.issues` (double nesting). - -**Fix:** In the `case 'error':` block of `rewriteCapturedSafeParse`, check if the parent node is another PropertyAccessExpression (meaning `r.error.X`). Handle `.issues` (unwrap) and `.message` (rewrite to array map) specifically. - -**Impact:** Fixes ~10 TypeScript errors in inspector/client's `AppRenderer.tsx`, `SamplingRequest.tsx`, `ToolResults.tsx`. - -**Files:** -- Modify: `packages/codemod/src/migrations/v1-to-v2/transforms/specSchemaAccess.ts` -- Test: `packages/codemod/test/v1-to-v2/transforms/specSchemaAccess.test.ts` - -- [ ] **Step 1: Write failing tests for error sub-property remapping** - -Add to `specSchemaAccess.test.ts` inside the "auto-transform: captured safeParse result" describe block: - -```typescript -it('rewrites .error.issues to .issues (unwrap double nesting)', () => { - const input = [ - `import { CallToolResultSchema } from '@modelcontextprotocol/server';`, - `const parsed = CallToolResultSchema.safeParse(data);`, - `if (!parsed.success) { console.log(parsed.error.issues); }`, - '' - ].join('\n'); - const { text } = applyTransform(input); - expect(text).toContain('parsed.issues'); - expect(text).not.toContain('parsed.issues.issues'); - expect(text).not.toContain('parsed.error'); -}); - -it('rewrites .error.message to issues map expression', () => { - const input = [ - `import { CallToolResultSchema } from '@modelcontextprotocol/server';`, - `const parsed = CallToolResultSchema.safeParse(data);`, - `if (!parsed.success) { console.log(parsed.error.message); }`, - '' - ].join('\n'); - const { text } = applyTransform(input); - expect(text).not.toContain('parsed.error'); - expect(text).not.toContain('parsed.issues.message'); - expect(text).toContain("parsed.issues?.map(i => i.message).join(', ')"); -}); - -it('rewrites bare .error to .issues (unchanged behavior)', () => { - const input = [ - `import { ToolSchema } from '@modelcontextprotocol/server';`, - `const result = ToolSchema.safeParse(raw);`, - `if (!result.success) { console.log(result.error); }`, - '' - ].join('\n'); - const { text } = applyTransform(input); - expect(text).toContain('result.issues'); - expect(text).not.toContain('result.error'); -}); -``` - -- [ ] **Step 2: Run tests to verify they fail** - -Run: `pnpm --filter @modelcontextprotocol/codemod test -- test/v1-to-v2/transforms/specSchemaAccess.test.ts` -Expected: First 2 new tests FAIL (`.error.issues` becomes `.issues.issues`, `.error.message` becomes `.issues.message`). Third test should already pass. - -- [ ] **Step 3: Update the error case in rewriteCapturedSafeParse** - -In `packages/codemod/src/migrations/v1-to-v2/transforms/specSchemaAccess.ts`, in the `rewriteCapturedSafeParse` function, replace the `case 'error'` block (around line 293): - -```typescript -// BEFORE: -case 'error': { - replacements.push({ node, newText: `${varName}.issues` }); - break; -} -``` - -with: - -```typescript -// AFTER: -case 'error': { - const errorParent = node.getParent(); - if (errorParent && Node.isPropertyAccessExpression(errorParent) && errorParent.getExpression() === node) { - const subProp = errorParent.getName(); - if (subProp === 'issues') { - replacements.push({ node: errorParent, newText: `${varName}.issues` }); - } else if (subProp === 'message') { - replacements.push({ node: errorParent, newText: `${varName}.issues?.map(i => i.message).join(', ')` }); - } else { - replacements.push({ node: errorParent, newText: `${varName}.issues` }); - } - } else { - replacements.push({ node, newText: `${varName}.issues` }); - } - break; -} -``` - -- [ ] **Step 4: Run tests to verify they pass** - -Run: `pnpm --filter @modelcontextprotocol/codemod test -- test/v1-to-v2/transforms/specSchemaAccess.test.ts` -Expected: All tests PASS - -- [ ] **Step 5: Commit** - -```bash -git add packages/codemod/src/migrations/v1-to-v2/transforms/specSchemaAccess.ts packages/codemod/test/v1-to-v2/transforms/specSchemaAccess.test.ts -git commit -m "fix(codemod): handle .error sub-property accesses in safeParse rewrite" -``` - ---- - -### Task 4: Handle `zod-compat.js` import path - -The import path `@modelcontextprotocol/sdk/server/zod-compat.js` is not in `IMPORT_MAP`, so `importPaths` emits "Unknown SDK import path" and leaves it untouched. The file exported `AnySchema` and `SchemaOutput` types that don't exist in v2. - -**Fix:** Add the path to `IMPORT_MAP` as `removed` with a descriptive message. This removes the import and emits a clear diagnostic. - -**Impact:** Fixes "Unknown SDK import path" warnings in inspector/client (4 files). The `AnySchema`/`SchemaOutput` usages in function signatures will still need manual migration, but the import won't be stale. - -**Files:** -- Modify: `packages/codemod/src/migrations/v1-to-v2/mappings/importMap.ts` -- Test: `packages/codemod/test/v1-to-v2/transforms/importPaths.test.ts` - -- [ ] **Step 1: Write failing test for zod-compat import removal** - -Add to `importPaths.test.ts`: - -```typescript -it('removes zod-compat.js import and emits diagnostic', () => { - const input = [ - `import { AnySchema, SchemaOutput } from '@modelcontextprotocol/sdk/server/zod-compat.js';`, - `function validate(schema: T): SchemaOutput { return {} as any; }`, - '' - ].join('\n'); - const project = new Project({ useInMemoryFileSystem: true }); - const sourceFile = project.createSourceFile('test.ts', input); - const result = importPathsTransform.apply(sourceFile, ctx); - const text = sourceFile.getFullText(); - expect(text).not.toContain('zod-compat'); - expect(text).not.toContain("from '@modelcontextprotocol/sdk"); - expect(result.changesCount).toBeGreaterThan(0); - expect(result.diagnostics.length).toBeGreaterThan(0); - expect(result.diagnostics[0]!.message).toContain('zod-compat'); -}); -``` - -Ensure the test file imports the necessary pieces — check the existing test imports at the top and match them. The existing test file should already import `importPathsTransform`, `Project`, and define a `ctx` constant. - -- [ ] **Step 2: Run test to verify it fails** - -Run: `pnpm --filter @modelcontextprotocol/codemod test -- test/v1-to-v2/transforms/importPaths.test.ts` -Expected: FAIL — import is left unchanged, "Unknown SDK import path" warning emitted - -- [ ] **Step 3: Add zod-compat.js to the import map** - -In `packages/codemod/src/migrations/v1-to-v2/mappings/importMap.ts`, add this entry to `IMPORT_MAP` after the `'@modelcontextprotocol/sdk/server/middleware.js'` entry: - -```typescript -'@modelcontextprotocol/sdk/server/zod-compat.js': { - target: '', - status: 'removed', - removalMessage: - 'zod-compat removed in v2. AnySchema and SchemaOutput types have no v2 equivalent — v2 uses StandardSchemaV1 from @standard-schema/spec. Rewrite generic function signatures to use StandardSchemaV1 directly.' -}, -``` - -- [ ] **Step 4: Run test to verify it passes** - -Run: `pnpm --filter @modelcontextprotocol/codemod test -- test/v1-to-v2/transforms/importPaths.test.ts` -Expected: All tests PASS - -- [ ] **Step 5: Commit** - -```bash -git add packages/codemod/src/migrations/v1-to-v2/mappings/importMap.ts packages/codemod/test/v1-to-v2/transforms/importPaths.test.ts -git commit -m "fix(codemod): handle zod-compat.js import path as removed" -``` - ---- - -### Task 5: Rename `ResourceTemplate` type imports to `ResourceTemplateType` - -When `ResourceTemplate` is imported from `@modelcontextprotocol/sdk/types.js` (protocol type usage), the import is rewritten to `@modelcontextprotocol/server`. But the server exports a `ResourceTemplate` **class** (used for server-side registration), shadowing the protocol type. The protocol type already exists in v2 as `ResourceTemplateType` (defined in `core/src/types/types.ts`, publicly exported via `core/public`'s `export * from '../../types/types.js'`, and re-exported by both `@modelcontextprotocol/server` and `@modelcontextprotocol/client`). - -**Fix:** Add `ResourceTemplate` → `ResourceTemplateType` to the `renamedSymbols` mapping for the `types.js` import path. This auto-renames the import and all references. No SDK changes needed — `ResourceTemplateType` is already publicly exported. - -**Impact:** Fixes ~8 TypeScript errors in inspector/client `ResourcesTab.tsx` (`.name`, `.description`, `UriTemplate` vs `string` issues). - -**Files:** -- Modify: `packages/codemod/src/migrations/v1-to-v2/mappings/importMap.ts` -- Test: `packages/codemod/test/v1-to-v2/transforms/importPaths.test.ts` - -- [ ] **Step 1: Write failing test for ResourceTemplate rename** - -Add to `importPaths.test.ts`: - -```typescript -it('renames ResourceTemplate to ResourceTemplateType when imported from types.js', () => { - const input = [ - `import { ResourceTemplate } from '@modelcontextprotocol/sdk/types.js';`, - `const template: ResourceTemplate = getTemplate();`, - '' - ].join('\n'); - const project = new Project({ useInMemoryFileSystem: true }); - const sourceFile = project.createSourceFile('test.ts', input); - const result = importPathsTransform.apply(sourceFile, ctx); - const text = sourceFile.getFullText(); - expect(text).toContain('ResourceTemplateType'); - expect(text).not.toMatch(/\bResourceTemplate\b(?!Type)/); - expect(result.changesCount).toBeGreaterThan(0); -}); -``` - -- [ ] **Step 2: Run test to verify it fails** - -Run: `pnpm --filter @modelcontextprotocol/codemod test -- test/v1-to-v2/transforms/importPaths.test.ts` -Expected: FAIL — ResourceTemplate is not renamed - -- [ ] **Step 3: Add ResourceTemplate rename to import map** - -In `packages/codemod/src/migrations/v1-to-v2/mappings/importMap.ts`, find the entry for `'@modelcontextprotocol/sdk/types.js'`: - -```typescript -'@modelcontextprotocol/sdk/types.js': { - target: 'RESOLVE_BY_CONTEXT', - status: 'moved' -}, -``` - -Add `renamedSymbols`: - -```typescript -'@modelcontextprotocol/sdk/types.js': { - target: 'RESOLVE_BY_CONTEXT', - status: 'moved', - renamedSymbols: { - ResourceTemplate: 'ResourceTemplateType' - } -}, -``` - -- [ ] **Step 4: Run test to verify it passes** - -Run: `pnpm --filter @modelcontextprotocol/codemod test -- test/v1-to-v2/transforms/importPaths.test.ts` -Expected: All tests PASS - -- [ ] **Step 5: Run full test suite** - -Run: `pnpm --filter @modelcontextprotocol/codemod test` -Expected: All tests PASS across all test files - -- [ ] **Step 6: Commit** - -```bash -git add packages/codemod/src/migrations/v1-to-v2/mappings/importMap.ts packages/codemod/test/v1-to-v2/transforms/importPaths.test.ts -git commit -m "fix(codemod): rename ResourceTemplate to ResourceTemplateType to avoid class collision" -``` - ---- - -## Verification - -After all 5 tasks are complete: - -- [ ] **Rebuild and re-run batch test** - -```bash -pnpm --filter @modelcontextprotocol/codemod build -pnpm --filter @modelcontextprotocol/codemod batch-test -``` - -Compare `packages/codemod/batch-test/results/summary.json` with the pre-fix results. Expected improvements: -- inspector/client: build errors should decrease significantly (StandardSchemaV1→AnySchema errors from handler registration fixed, schema import errors fixed) -- inspector/server: `SSEServerTransport` errors remain (manual migration), but `setRequestHandler` task schema errors should be fixed -- mcp-servers-fork: `SSEServerTransport` errors remain (manual migration), test context mock errors remain (manual migration) diff --git a/docs/superpowers/plans/2026-06-02-readbuffer-max-size.md b/docs/superpowers/plans/2026-06-02-readbuffer-max-size.md deleted file mode 100644 index 867e8a3599..0000000000 --- a/docs/superpowers/plans/2026-06-02-readbuffer-max-size.md +++ /dev/null @@ -1,323 +0,0 @@ -# ReadBuffer Max Size Guard Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Add a configurable maximum buffer size to `ReadBuffer` to prevent unbounded memory growth from a misbehaving stdio peer (GHSA-wqgc-pwpr-pq7r). - -**Architecture:** `ReadBuffer.append()` gains a size guard that throws on overflow. Both stdio transports wrap their data handlers in try/catch to catch the throw, report via `onerror`, and close the transport. The constant and constructor option are exported as public API. - -**Tech Stack:** TypeScript, vitest - ---- - -### Task 1: Add size guard to ReadBuffer - -**Files:** -- Modify: `packages/core/src/shared/stdio.ts:1-42` - -- [ ] **Step 1: Write failing tests for buffer overflow** - -Add a new `describe` block to `packages/core/test/shared/stdio.test.ts`: - -```typescript -describe('buffer size limit', () => { - test('should throw when buffer exceeds default max size', () => { - const readBuffer = new ReadBuffer(); - const chunk = Buffer.alloc(1024 * 1024); // 1 MB - // Default is 10 MB, so 11 appends should fail - for (let i = 0; i < 10; i++) { - readBuffer.append(chunk); - } - expect(() => readBuffer.append(chunk)).toThrow( - /ReadBuffer exceeded maximum size/ - ); - }); - - test('should throw when buffer exceeds custom max size', () => { - const readBuffer = new ReadBuffer({ maxBufferSize: 100 }); - readBuffer.append(Buffer.alloc(50)); - expect(() => readBuffer.append(Buffer.alloc(51))).toThrow( - /ReadBuffer exceeded maximum size/ - ); - }); - - test('should clear buffer before throwing on overflow', () => { - const readBuffer = new ReadBuffer({ maxBufferSize: 100 }); - readBuffer.append(Buffer.alloc(50)); - expect(() => readBuffer.append(Buffer.alloc(51))).toThrow(); - - // Buffer should be cleared — can append again - readBuffer.append(Buffer.alloc(50)); - // And read messages normally - expect(readBuffer.readMessage()).toBeNull(); - }); - - test('should allow appending up to exactly the max size', () => { - const readBuffer = new ReadBuffer({ maxBufferSize: 100 }); - // Should not throw — exactly at limit - expect(() => readBuffer.append(Buffer.alloc(100))).not.toThrow(); - }); - - test('should work with no options (backwards compatible)', () => { - const readBuffer = new ReadBuffer(); - // Small append should always work - readBuffer.append(Buffer.from('hello\n')); - expect(readBuffer.readMessage()).not.toBeNull(); - }); -}); -``` - -- [ ] **Step 2: Run the tests to confirm they fail** - -Run: `pnpm --filter @modelcontextprotocol/core test -- packages/core/test/shared/stdio.test.ts` -Expected: FAIL — `ReadBuffer` constructor doesn't accept options yet. - -- [ ] **Step 3: Implement the size guard in ReadBuffer** - -Modify `packages/core/src/shared/stdio.ts`. The full file should become: - -```typescript -import type { JSONRPCMessage } from '../types/index.js'; -import { JSONRPCMessageSchema } from '../types/index.js'; - -export const DEFAULT_MAX_BUFFER_SIZE = 10 * 1024 * 1024; // 10 MB - -/** - * Buffers a continuous stdio stream into discrete JSON-RPC messages. - */ -export class ReadBuffer { - private _buffer?: Buffer; - private _maxBufferSize: number; - - constructor(options?: { maxBufferSize?: number }) { - this._maxBufferSize = options?.maxBufferSize ?? DEFAULT_MAX_BUFFER_SIZE; - } - - append(chunk: Buffer): void { - const newSize = (this._buffer?.length ?? 0) + chunk.length; - if (newSize > this._maxBufferSize) { - this.clear(); - throw new Error( - `ReadBuffer exceeded maximum size of ${this._maxBufferSize} bytes` - ); - } - this._buffer = this._buffer ? Buffer.concat([this._buffer, chunk]) : chunk; - } - - readMessage(): JSONRPCMessage | null { - while (this._buffer) { - const index = this._buffer.indexOf('\n'); - if (index === -1) { - return null; - } - - const line = this._buffer.toString('utf8', 0, index).replace(/\r$/, ''); - this._buffer = this._buffer.subarray(index + 1); - - try { - return deserializeMessage(line); - } catch (error) { - // Skip non-JSON lines (e.g., debug output from hot-reload tools like - // tsx or nodemon that write to stdout). Schema validation errors still - // throw so malformed-but-valid-JSON messages surface via onerror. - if (error instanceof SyntaxError) { - continue; - } - throw error; - } - } - return null; - } - - clear(): void { - this._buffer = undefined; - } -} - -export function deserializeMessage(line: string): JSONRPCMessage { - return JSONRPCMessageSchema.parse(JSON.parse(line)); -} - -export function serializeMessage(message: JSONRPCMessage): string { - return JSON.stringify(message) + '\n'; -} -``` - -- [ ] **Step 4: Run the tests to confirm they pass** - -Run: `pnpm --filter @modelcontextprotocol/core test -- packages/core/test/shared/stdio.test.ts` -Expected: All tests PASS (including all existing tests — backwards compatible). - -- [ ] **Step 5: Commit** - -```bash -git add packages/core/src/shared/stdio.ts packages/core/test/shared/stdio.test.ts -git commit -m "fix(core): add max buffer size guard to ReadBuffer - -Prevents unbounded memory growth when a stdio peer sends data without -newline delimiters. Default limit is 10 MB, configurable via constructor. - -Ref: GHSA-wqgc-pwpr-pq7r" -``` - ---- - -### Task 2: Add DEFAULT_MAX_BUFFER_SIZE to public exports - -**Files:** -- Modify: `packages/core/src/exports/public/index.ts:70` - -- [ ] **Step 1: Add the constant to the public export** - -Change line 70 in `packages/core/src/exports/public/index.ts` from: - -```typescript -export { deserializeMessage, ReadBuffer, serializeMessage } from '../../shared/stdio.js'; -``` - -to: - -```typescript -export { DEFAULT_MAX_BUFFER_SIZE, deserializeMessage, ReadBuffer, serializeMessage } from '../../shared/stdio.js'; -``` - -- [ ] **Step 2: Run typecheck to confirm it compiles** - -Run: `pnpm --filter @modelcontextprotocol/core typecheck` -Expected: No errors. - -- [ ] **Step 3: Commit** - -```bash -git add packages/core/src/exports/public/index.ts -git commit -m "feat(core): export DEFAULT_MAX_BUFFER_SIZE from public API" -``` - ---- - -### Task 3: Add try/catch to StdioClientTransport data handler - -**Files:** -- Modify: `packages/client/src/client/stdio.ts:151-154` - -- [ ] **Step 1: Wrap the data handler in try/catch** - -Change lines 151-154 of `packages/client/src/client/stdio.ts` from: - -```typescript - this._process.stdout?.on('data', chunk => { - this._readBuffer.append(chunk); - this.processReadBuffer(); - }); -``` - -to: - -```typescript - this._process.stdout?.on('data', chunk => { - try { - this._readBuffer.append(chunk); - this.processReadBuffer(); - } catch (error) { - this.onerror?.(error as Error); - this.close().catch(() => {}); - } - }); -``` - -- [ ] **Step 2: Run typecheck** - -Run: `pnpm --filter @modelcontextprotocol/client typecheck` -Expected: No errors. - -- [ ] **Step 3: Run existing stdio client tests to verify no regression** - -Run: `pnpm --filter @modelcontextprotocol/client test -- packages/client/test/client/stdio.test.ts` -Expected: All existing tests PASS. - -- [ ] **Step 4: Commit** - -```bash -git add packages/client/src/client/stdio.ts -git commit -m "fix(client): catch ReadBuffer overflow in StdioClientTransport data handler - -Prevents an uncaught exception when ReadBuffer.append() throws due to -exceeding the max buffer size. Routes the error to onerror and closes -the transport. - -Ref: GHSA-wqgc-pwpr-pq7r" -``` - ---- - -### Task 4: Add try/catch to StdioServerTransport data handler - -**Files:** -- Modify: `packages/server/src/server/stdio.ts:34-37` - -- [ ] **Step 1: Wrap the _ondata handler in try/catch** - -Change lines 34-37 of `packages/server/src/server/stdio.ts` from: - -```typescript - _ondata = (chunk: Buffer) => { - this._readBuffer.append(chunk); - this.processReadBuffer(); - }; -``` - -to: - -```typescript - _ondata = (chunk: Buffer) => { - try { - this._readBuffer.append(chunk); - this.processReadBuffer(); - } catch (error) { - this.onerror?.(error as Error); - this.close().catch(() => {}); - } - }; -``` - -- [ ] **Step 2: Run typecheck** - -Run: `pnpm --filter @modelcontextprotocol/server typecheck` -Expected: No errors. - -- [ ] **Step 3: Run existing stdio server tests to verify no regression** - -Run: `pnpm --filter @modelcontextprotocol/server test -- packages/server/test/server/stdio.test.ts` -Expected: All existing tests PASS. - -- [ ] **Step 4: Commit** - -```bash -git add packages/server/src/server/stdio.ts -git commit -m "fix(server): catch ReadBuffer overflow in StdioServerTransport data handler - -Prevents an uncaught exception when ReadBuffer.append() throws due to -exceeding the max buffer size. Routes the error to onerror and closes -the transport. - -Ref: GHSA-wqgc-pwpr-pq7r" -``` - ---- - -### Task 5: Full test suite verification - -- [ ] **Step 1: Run full typecheck across all packages** - -Run: `pnpm typecheck:all` -Expected: No errors. - -- [ ] **Step 2: Run full test suite** - -Run: `pnpm test:all` -Expected: All tests PASS. - -- [ ] **Step 3: Run lint** - -Run: `pnpm lint:all` -Expected: No errors (or fix any formatting issues with `pnpm lint:fix:all`). diff --git a/docs/superpowers/plans/2026-06-02-v1-readbuffer-max-size.md b/docs/superpowers/plans/2026-06-02-v1-readbuffer-max-size.md deleted file mode 100644 index cfbbfb23b9..0000000000 --- a/docs/superpowers/plans/2026-06-02-v1-readbuffer-max-size.md +++ /dev/null @@ -1,356 +0,0 @@ -# V1 ReadBuffer Max Size Guard Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Port the ReadBuffer max size guard from the v2 branch (`fix/stdio-buffer-limit`, commit `08780873`) to v1. This prevents unbounded memory growth when a misbehaving stdio peer sends data without newline delimiters (GHSA-wqgc-pwpr-pq7r). - -**Architecture:** `ReadBuffer.append()` gains a size guard that throws on overflow. Both stdio transports wrap their data handlers in try/catch to catch the throw, report via `onerror`, and close the transport. The constant `STDIO_DEFAULT_MAX_BUFFER_SIZE` is exported from `src/shared/stdio.ts`. - -**Tech Stack:** TypeScript, vitest - -**Key difference from v2:** V1 is a flat `src/` layout (not a monorepo under `packages/`). There is no public re-export index file, so the constant is only exported from `src/shared/stdio.ts` directly. - ---- - -### Task 1: Add size guard to ReadBuffer - -**Files:** -- Modify: `src/shared/stdio.ts` -- Modify: `test/shared/stdio.test.ts` - -- [ ] **Step 1: Add buffer size limit tests** - -Append the following to `test/shared/stdio.test.ts`: - -```typescript -import { STDIO_DEFAULT_MAX_BUFFER_SIZE } from '../../src/shared/stdio.js'; - -describe('buffer size limit', () => { - test('should throw when buffer exceeds default max size', () => { - const readBuffer = new ReadBuffer(); - const chunkSize = 1024 * 1024; // 1 MB - const chunk = Buffer.alloc(chunkSize); - const chunksToFill = Math.floor(STDIO_DEFAULT_MAX_BUFFER_SIZE / chunkSize); - for (let i = 0; i < chunksToFill; i++) { - readBuffer.append(chunk); - } - expect(() => readBuffer.append(chunk)).toThrow( - /ReadBuffer exceeded maximum size/ - ); - }); - - test('should throw when buffer exceeds custom max size', () => { - const readBuffer = new ReadBuffer({ maxBufferSize: 100 }); - readBuffer.append(Buffer.alloc(50)); - expect(() => readBuffer.append(Buffer.alloc(51))).toThrow( - /ReadBuffer exceeded maximum size/ - ); - }); - - test('should clear buffer before throwing on overflow', () => { - const readBuffer = new ReadBuffer({ maxBufferSize: 100 }); - readBuffer.append(Buffer.alloc(50)); - expect(() => readBuffer.append(Buffer.alloc(51))).toThrow(); - - // Buffer should be cleared — can append again - readBuffer.append(Buffer.alloc(50)); - // And read messages normally - expect(readBuffer.readMessage()).toBeNull(); - }); - - test('should allow appending up to exactly the max size', () => { - const readBuffer = new ReadBuffer({ maxBufferSize: 100 }); - // Should not throw — exactly at limit - expect(() => readBuffer.append(Buffer.alloc(100))).not.toThrow(); - }); - - test('should work with no options (backwards compatible)', () => { - const readBuffer = new ReadBuffer(); - // Small append should always work - readBuffer.append(Buffer.from(JSON.stringify({ jsonrpc: '2.0', method: 'ping' }) + '\n')); - expect(readBuffer.readMessage()).not.toBeNull(); - }); -}); -``` - -Also update the existing import at the top of the file — change: - -```typescript -import { ReadBuffer } from '../../src/shared/stdio.js'; -``` - -to: - -```typescript -import { STDIO_DEFAULT_MAX_BUFFER_SIZE, ReadBuffer } from '../../src/shared/stdio.js'; -``` - -- [ ] **Step 2: Run tests to confirm they fail** - -Run: `npx vitest test/shared/stdio.test.ts --run` -Expected: FAIL — `ReadBuffer` constructor doesn't accept options yet, `STDIO_DEFAULT_MAX_BUFFER_SIZE` doesn't exist. - -- [ ] **Step 3: Implement the size guard in ReadBuffer** - -Modify `src/shared/stdio.ts`. Add the constant and constructor, and add the size guard to `append()`. The full file should become: - -```typescript -import { JSONRPCMessage, JSONRPCMessageSchema } from '../types.js'; - -export const STDIO_DEFAULT_MAX_BUFFER_SIZE = 10 * 1024 * 1024; - -/** - * Buffers a continuous stdio stream into discrete JSON-RPC messages. - */ -export class ReadBuffer { - private _buffer?: Buffer; - private _maxBufferSize: number; - - constructor(options?: { maxBufferSize?: number }) { - this._maxBufferSize = options?.maxBufferSize ?? STDIO_DEFAULT_MAX_BUFFER_SIZE; - } - - append(chunk: Buffer): void { - const newSize = (this._buffer?.length ?? 0) + chunk.length; - if (newSize > this._maxBufferSize) { - this.clear(); - throw new Error( - `ReadBuffer exceeded maximum size of ${this._maxBufferSize} bytes` - ); - } - this._buffer = this._buffer ? Buffer.concat([this._buffer, chunk]) : chunk; - } - - readMessage(): JSONRPCMessage | null { - if (!this._buffer) { - return null; - } - - const index = this._buffer.indexOf('\n'); - if (index === -1) { - return null; - } - - const line = this._buffer.toString('utf8', 0, index).replace(/\r$/, ''); - this._buffer = this._buffer.subarray(index + 1); - return deserializeMessage(line); - } - - clear(): void { - this._buffer = undefined; - } -} - -export function deserializeMessage(line: string): JSONRPCMessage { - return JSONRPCMessageSchema.parse(JSON.parse(line)); -} - -export function serializeMessage(message: JSONRPCMessage): string { - return JSON.stringify(message) + '\n'; -} -``` - -- [ ] **Step 4: Run tests to confirm they pass** - -Run: `npx vitest test/shared/stdio.test.ts --run` -Expected: All tests PASS. - -- [ ] **Step 5: Suggest commit** - -```bash -git add src/shared/stdio.ts test/shared/stdio.test.ts -git commit -m "fix: add max buffer size guard to ReadBuffer - -Prevents unbounded memory growth when a stdio peer sends data without -newline delimiters. Default limit is 10 MB, configurable via constructor. - -Ref: GHSA-wqgc-pwpr-pq7r" -``` - ---- - -### Task 2: Add try/catch to StdioServerTransport data handler - -**Files:** -- Modify: `src/server/stdio.ts:26-29` -- Modify: `test/server/stdio.test.ts` - -- [ ] **Step 1: Add overflow test for StdioServerTransport** - -Append the following test to `test/server/stdio.test.ts`: - -```typescript -test('should fire onerror and close when ReadBuffer overflows', async () => { - const server = new StdioServerTransport(input, output); - - let receivedError: Error | undefined; - server.onerror = err => { - receivedError = err; - }; - let closeCount = 0; - server.onclose = () => { - closeCount++; - }; - - await server.start(); - - // Push data exceeding the default 10 MB limit without a newline - const chunk = Buffer.alloc(11 * 1024 * 1024, 0x41); - input.push(chunk); - - // Allow the close() promise to settle - await new Promise(resolve => setTimeout(resolve, 10)); - - expect(receivedError?.message).toMatch(/ReadBuffer exceeded maximum size/); - expect(closeCount).toBe(1); -}); -``` - -- [ ] **Step 2: Run to confirm the test fails** - -Run: `npx vitest test/server/stdio.test.ts --run` -Expected: FAIL — the uncaught throw from `append()` crashes instead of being caught. - -- [ ] **Step 3: Wrap the _ondata handler in try/catch** - -Change lines 26-29 of `src/server/stdio.ts` from: - -```typescript - _ondata = (chunk: Buffer) => { - this._readBuffer.append(chunk); - this.processReadBuffer(); - }; -``` - -to: - -```typescript - _ondata = (chunk: Buffer) => { - try { - this._readBuffer.append(chunk); - this.processReadBuffer(); - } catch (error) { - this.onerror?.(error as Error); - this.close().catch(() => {}); - } - }; -``` - -- [ ] **Step 4: Run tests to confirm they pass** - -Run: `npx vitest test/server/stdio.test.ts --run` -Expected: All tests PASS. - -- [ ] **Step 5: Suggest commit** - -```bash -git add src/server/stdio.ts test/server/stdio.test.ts -git commit -m "fix(server): catch ReadBuffer overflow in StdioServerTransport - -Prevents an uncaught exception when ReadBuffer.append() throws due to -exceeding the max buffer size. Routes the error to onerror and closes -the transport. - -Ref: GHSA-wqgc-pwpr-pq7r" -``` - ---- - -### Task 3: Add try/catch to StdioClientTransport data handler - -**Files:** -- Modify: `src/client/stdio.ts:150-153` -- Modify: `test/client/stdio.test.ts` - -- [ ] **Step 1: Add overflow test for StdioClientTransport** - -Append the following test to `test/client/stdio.test.ts`: - -```typescript -test('should fire onerror and close when ReadBuffer overflows', async () => { - const client = new StdioClientTransport({ - command: 'node', - args: ['-e', 'process.stdout.write(Buffer.alloc(11 * 1024 * 1024, 0x41))'] - }); - - const errorReceived = new Promise(resolve => { - client.onerror = resolve; - }); - const closed = new Promise(resolve => { - client.onclose = () => resolve(); - }); - - await client.start(); - - const error = await errorReceived; - expect(error.message).toMatch(/ReadBuffer exceeded maximum size/); - await closed; -}); -``` - -- [ ] **Step 2: Run to confirm the test fails** - -Run: `npx vitest test/client/stdio.test.ts --run` -Expected: FAIL — the uncaught throw from `append()` crashes. - -- [ ] **Step 3: Wrap the stdout data handler in try/catch** - -Change lines 150-153 of `src/client/stdio.ts` from: - -```typescript - this._process.stdout?.on('data', chunk => { - this._readBuffer.append(chunk); - this.processReadBuffer(); - }); -``` - -to: - -```typescript - this._process.stdout?.on('data', chunk => { - try { - this._readBuffer.append(chunk); - this.processReadBuffer(); - } catch (error) { - this.onerror?.(error as Error); - this.close().catch(() => {}); - } - }); -``` - -- [ ] **Step 4: Run tests to confirm they pass** - -Run: `npx vitest test/client/stdio.test.ts --run` -Expected: All tests PASS. - -- [ ] **Step 5: Suggest commit** - -```bash -git add src/client/stdio.ts test/client/stdio.test.ts -git commit -m "fix(client): catch ReadBuffer overflow in StdioClientTransport - -Prevents an uncaught exception when ReadBuffer.append() throws due to -exceeding the max buffer size. Routes the error to onerror and closes -the transport. - -Ref: GHSA-wqgc-pwpr-pq7r" -``` - ---- - -### Task 4: Full verification - -- [ ] **Step 1: Run typecheck** - -Run: `npm run typecheck` -Expected: No errors. - -- [ ] **Step 2: Run full test suite** - -Run: `npm test` -Expected: All tests PASS. - -- [ ] **Step 3: Run lint** - -Run: `npm run lint` -Expected: No errors (or fix with `npm run lint:fix`). diff --git a/docs/superpowers/plans/2026-06-23-sdk-shared-package.md b/docs/superpowers/plans/2026-06-23-sdk-shared-package.md deleted file mode 100644 index c148328705..0000000000 --- a/docs/superpowers/plans/2026-06-23-sdk-shared-package.md +++ /dev/null @@ -1,716 +0,0 @@ -# `@modelcontextprotocol/sdk-shared` Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Extract the canonical MCP spec data model (Zod schemas + derived TS types + protocol constants) into a new publishable package `@modelcontextprotocol/sdk-shared`, so v1→v2 schema-validation migration becomes a mechanical import-path swap with `.parse`/`.safeParse`/all Zod methods preserved. - -**Architecture:** A new zod-only, runtime-neutral package owns `constants.ts` + `schemas.ts` + `types.ts` (moved from `core`). `core` keeps thin re-export shims at the old paths (churn control); `core/public`, `server`, and `client` re-export the **types** (Zod-free) and continue to expose `specTypeSchemas` unchanged; the raw Zod `*Schema` constants are reachable only from `sdk-shared`. The codemod routes `@modelcontextprotocol/sdk/types.js` → `@modelcontextprotocol/sdk-shared` as a fixed path swap and drops the `specSchemaAccess` rewriting entirely. - -**Tech Stack:** TypeScript (NodeNext, `tsgo` typecheck), Zod v4, tsdown (build, ESM `.mjs`/`.d.mts`), vitest, ts-morph (codemod), changesets (prerelease `alpha` mode), pnpm workspaces. - -## Global Constraints - -- Node engine floor: `>=20`. Package version line: `2.0.0-alpha.2` (match other runtime packages). -- Formatting (Prettier, `.prettierrc.json`): 4-space indent, single quotes, semicolons, **no trailing commas**, print width 140. All new/edited files must satisfy `prettier --check`. -- Source imports use explicit `.js` extensions (NodeNext); sibling `.ts` files import each other as `./x.js`. -- Public API uses **explicit named exports** except `types.ts`, which is the one intentional `export *` (it contains only spec-derived TS types). -- `sdk-shared` must be **runtime-neutral** (no Node builtins) — guarded by a `barrelClean` test. -- `sdk-shared`'s only runtime dependency is `zod` (`catalog:runtimeShared` → `^4.2.0`). No `publishConfig` (root `.npmrc` + changesets `access: public` handle it). -- Never run `git add`/`git commit` (a hook blocks it). At each "Commit" step, **print the suggested commands** for the user to run manually. -- Typecheck per package: `tsgo -p tsconfig.json --noEmit`. Tests: `vitest run` (tests live in `test/**/*.test.ts`, not colocated). - ---- - -## File Structure - -**New package `packages/sdk-shared/`:** -- `package.json`, `tsconfig.json`, `tsdown.config.ts`, `vitest.config.js`, `eslint.config.mjs`, `README.md` -- `src/constants.ts`, `src/schemas.ts`, `src/types.ts` — relocated from `packages/core/src/types/` -- `src/index.ts` — main barrel: types + constants + schemas (everything; first-class Zod) -- `test/barrelClean.test.ts` — runtime-neutrality guard -- The `./types` subpath is served directly by the built `src/types.ts` (types-only; Zod-free) for `core/public` to re-export. - -**Modified in `packages/core/`:** -- `src/types/constants.ts`, `src/types/schemas.ts`, `src/types/types.ts` → become 1-line re-export shims pointing at `sdk-shared` (churn control) -- `src/exports/public/index.ts` → re-point the types `export *` and the constants named-export at `sdk-shared` -- `package.json` → add `@modelcontextprotocol/sdk-shared` dependency -- `src/types/specTypeSchema.ts` → its `import * as schemas from './schemas.js'` keeps working via the shim (no edit needed if shim is in place) - -**Modified in `packages/server/`, `packages/client/`:** -- `package.json` → add `@modelcontextprotocol/sdk-shared` dependency -- `tsdown.config.ts` → add `@modelcontextprotocol/sdk-shared` to `external`; add its `src` path to the dts `paths` so `.d.mts` resolves - -**Modified in `packages/codemod/`:** -- `scripts/generateVersions.ts` → add `sdk-shared` to `PACKAGE_DIRS`; regenerate `src/generated/versions.ts` -- `src/migrations/v1-to-v2/mappings/importMap.ts` → `sdk/types.js` target becomes `@modelcontextprotocol/sdk-shared` -- `src/migrations/v1-to-v2/transforms/index.ts` → remove `specSchemaAccess` from the pipeline -- delete `src/migrations/v1-to-v2/transforms/specSchemaAccess.ts` + `test/v1-to-v2/transforms/specSchemaAccess.test.ts` -- update `test/v1-to-v2/transforms/importPaths.test.ts` and any integration test expecting `specTypeSchemas` output -- `src/bin/batchTest.ts` → add `sdk-shared` to `LOCAL_PACKAGE_DIRS`; add `overrides` so transitive `server→sdk-shared` resolves to the local tarball - -**Modified docs / release:** -- `docs/migration.md`, `docs/migration-SKILL.md` → rewrite spec-schema validation section -- `.changeset/pre.json` → add `sdk-shared` to `initialVersions`; new `.changeset/add-sdk-shared-package.md` - ---- - -## Phase 1 — Create `sdk-shared`, move the spec data model, rewire consumers - -### Task 1.1: Scaffold the empty `sdk-shared` package - -**Files:** -- Create: `packages/sdk-shared/package.json`, `tsconfig.json`, `tsdown.config.ts`, `vitest.config.js`, `eslint.config.mjs`, `README.md`, `src/index.ts` -- Modify: `.changeset/pre.json` -- Create: `.changeset/add-sdk-shared-package.md` - -**Interfaces:** -- Produces: a buildable workspace package `@modelcontextprotocol/sdk-shared` whose `dist/index.mjs` + `dist/index.d.mts` exist. No real exports yet (placeholder). - -- [ ] **Step 1: Create `packages/sdk-shared/package.json`** - -```json -{ - "name": "@modelcontextprotocol/sdk-shared", - "private": false, - "version": "2.0.0-alpha.2", - "description": "Shared types and Zod schemas for the Model Context Protocol TypeScript SDK", - "license": "MIT", - "author": "Anthropic, PBC (https://anthropic.com)", - "homepage": "https://modelcontextprotocol.io", - "bugs": "https://github.com/modelcontextprotocol/typescript-sdk/issues", - "type": "module", - "repository": { - "type": "git", - "url": "git+https://github.com/modelcontextprotocol/typescript-sdk.git" - }, - "engines": { - "node": ">=20" - }, - "keywords": ["modelcontextprotocol", "mcp", "schemas", "types"], - "exports": { - ".": { - "types": "./dist/index.d.mts", - "import": "./dist/index.mjs" - }, - "./types": { - "types": "./dist/types.d.mts", - "import": "./dist/types.mjs" - } - }, - "types": "./dist/index.d.mts", - "typesVersions": { - "*": { - "types": ["dist/types.d.mts"] - } - }, - "files": ["dist"], - "scripts": { - "typecheck": "tsgo -p tsconfig.json --noEmit", - "build": "tsdown", - "build:watch": "tsdown --watch", - "prepack": "pnpm run build", - "lint": "eslint src/ && prettier --ignore-path ../../.prettierignore --check .", - "lint:fix": "eslint src/ --fix && prettier --ignore-path ../../.prettierignore --write .", - "check": "pnpm run typecheck && pnpm run lint", - "test": "vitest run", - "test:watch": "vitest" - }, - "dependencies": { - "zod": "catalog:runtimeShared" - }, - "devDependencies": { - "@modelcontextprotocol/eslint-config": "workspace:^", - "@modelcontextprotocol/tsconfig": "workspace:^", - "@modelcontextprotocol/vitest-config": "workspace:^", - "@eslint/js": "catalog:devTools", - "@typescript/native-preview": "catalog:devTools", - "eslint": "catalog:devTools", - "eslint-config-prettier": "catalog:devTools", - "eslint-plugin-n": "catalog:devTools", - "prettier": "catalog:devTools", - "tsdown": "catalog:devTools", - "typescript": "catalog:devTools", - "typescript-eslint": "catalog:devTools", - "vitest": "catalog:devTools" - } -} -``` - -- [ ] **Step 2: Create `packages/sdk-shared/tsconfig.json`** - -```json -{ - "extends": "@modelcontextprotocol/tsconfig", - "include": ["./"], - "exclude": ["node_modules", "dist"], - "compilerOptions": { - "paths": { "*": ["./*"] } - } -} -``` - -- [ ] **Step 3: Create `packages/sdk-shared/tsdown.config.ts`** (two entries: main + the types-only subpath) - -```ts -import { defineConfig } from 'tsdown'; - -export default defineConfig({ - failOnWarn: 'ci-only', - entry: ['src/index.ts', 'src/types.ts'], - format: ['esm'], - outDir: 'dist', - clean: true, - sourcemap: true, - target: 'esnext', - platform: 'node', - shims: true, - dts: { resolver: 'tsc' } -}); -``` - -- [ ] **Step 4: Create `packages/sdk-shared/vitest.config.js`** - -```js -import baseConfig from '@modelcontextprotocol/vitest-config'; - -export default baseConfig; -``` - -- [ ] **Step 5: Create `packages/sdk-shared/eslint.config.mjs`** - -```js -// @ts-check - -import baseConfig from '@modelcontextprotocol/eslint-config'; - -export default [ - ...baseConfig, - { - settings: { - 'import/internal-regex': '^@modelcontextprotocol/sdk-shared' - } - } -]; -``` - -- [ ] **Step 6: Create `packages/sdk-shared/README.md`** - -```md -# @modelcontextprotocol/sdk-shared - -Shared types and Zod schemas for the Model Context Protocol TypeScript SDK. Exposes the canonical MCP spec data model: the Zod `*Schema` constants, their derived TypeScript types, and protocol constants. - -- Import types and Zod schemas from `@modelcontextprotocol/sdk-shared`. -- For library-agnostic (Standard Schema) validation, prefer `specTypeSchemas` from `@modelcontextprotocol/server` / `@modelcontextprotocol/client`. -``` - -- [ ] **Step 7: Create placeholder `packages/sdk-shared/src/index.ts`** - -```ts -// Placeholder — real exports added in Task 1.2. -export const SDK_SHARED_PLACEHOLDER = true; -``` - -- [ ] **Step 8: Register the package in changesets prerelease state** — edit `.changeset/pre.json`, adding this entry to the `initialVersions` object (alphabetical position is fine): - -```json -"@modelcontextprotocol/sdk-shared": "2.0.0-alpha.0" -``` - -- [ ] **Step 9: Create `.changeset/add-sdk-shared-package.md`** - -```md ---- -'@modelcontextprotocol/sdk-shared': minor ---- - -Add @modelcontextprotocol/sdk-shared package: the canonical home for MCP spec Zod schemas, their derived TypeScript types, and protocol constants. -``` - -- [ ] **Step 10: Install + build to verify the scaffold** - -Run: `pnpm install && pnpm --filter @modelcontextprotocol/sdk-shared build` -Expected: install succeeds; build writes `packages/sdk-shared/dist/index.mjs` and `dist/index.d.mts` (and `dist/types.*`). Verify: `ls packages/sdk-shared/dist` shows `index.mjs index.d.mts types.mjs types.d.mts`. - -- [ ] **Step 11: Commit** (print for the user) - -```bash -git add packages/sdk-shared .changeset/pre.json .changeset/add-sdk-shared-package.md -git commit -m "feat(sdk-shared): scaffold empty @modelcontextprotocol/sdk-shared package" -``` - ---- - -### Task 1.2: Relocate the spec data model into `sdk-shared` - -**Files:** -- Move: `packages/core/src/types/constants.ts` → `packages/sdk-shared/src/constants.ts` -- Move: `packages/core/src/types/schemas.ts` → `packages/sdk-shared/src/schemas.ts` -- Move: `packages/core/src/types/types.ts` → `packages/sdk-shared/src/types.ts` -- Modify: `packages/sdk-shared/src/index.ts` - -**Interfaces:** -- Produces: `@modelcontextprotocol/sdk-shared` exports all spec types + all `*Schema` Zod constants + all protocol constants from `.`; `@modelcontextprotocol/sdk-shared/types` exports the spec **types only**. -- Consumes: nothing new (the three files are self-contained — only external import is `zod/v4`). - -- [ ] **Step 1: Move the three files** (preserves content + history) - -```bash -git mv packages/core/src/types/constants.ts packages/sdk-shared/src/constants.ts -git mv packages/core/src/types/schemas.ts packages/sdk-shared/src/schemas.ts -git mv packages/core/src/types/types.ts packages/sdk-shared/src/types.ts -``` - -The internal relative imports between these three files (`./constants.js`, `./types.js`, `./schemas.js`) and `zod/v4` remain valid in the new location — no edits needed inside them. Remove the `⚠️ PUBLIC API` comment header in `types.ts` that references `exports/public/index.ts` only if it is now inaccurate; otherwise leave it. - -- [ ] **Step 2: Write the real `packages/sdk-shared/src/index.ts`** (replace the placeholder) - -```ts -// Canonical MCP spec data model: protocol constants, spec-derived TS types, and the -// Zod *Schema constants. The `.` entry is the first-class public surface (Zod included). -// The types-only `./types` subpath is served by ./types.ts directly (see package.json exports). -export * from './constants.js'; -export * from './types.js'; -export * from './schemas.js'; -``` - -- [ ] **Step 3: Typecheck `sdk-shared` in isolation** - -Run: `pnpm --filter @modelcontextprotocol/sdk-shared typecheck` -Expected: PASS (no errors). If `tsgo` reports a missing import, it means a fourth file was part of the closure — re-check `schemas.ts`/`types.ts`/`constants.ts` imports and move any additional self-contained spec file. - -- [ ] **Step 4: Build `sdk-shared`** - -Run: `pnpm --filter @modelcontextprotocol/sdk-shared build` -Expected: PASS; `dist/index.mjs` now contains the schema runtime values; `dist/types.d.mts` exposes the 178 types. - -- [ ] **Step 5: Commit** (print for the user) - -```bash -git add packages/sdk-shared packages/core/src/types -git commit -m "feat(sdk-shared): move spec constants, schemas, and types into sdk-shared" -``` - ---- - -### Task 1.3: Rewire `core` to consume `sdk-shared` via re-export shims - -**Files:** -- Create (at the old paths): `packages/core/src/types/constants.ts`, `packages/core/src/types/schemas.ts`, `packages/core/src/types/types.ts` — now 1-line re-export shims -- Modify: `packages/core/package.json` (add dependency) -- Modify: `packages/core/src/exports/public/index.ts` (re-point types `export *`) -- Modify: `packages/core/tsconfig.json` (path mapping for `tsgo`, if needed) - -**Interfaces:** -- Consumes: `@modelcontextprotocol/sdk-shared` (`.` and `./types`). -- Produces: `core`'s internal relative imports of `./types.js`/`./schemas.js`/`./constants.js` keep resolving (via shims); `core/public` exports the same public symbols as before (types via `sdk-shared/types`, constants via `sdk-shared`, no schema values), so `server`/`client` surfaces are unchanged and Zod-free. - -- [ ] **Step 1: Add the dependency to `packages/core/package.json`** — add to `dependencies`: - -```json -"@modelcontextprotocol/sdk-shared": "workspace:^" -``` - -- [ ] **Step 2: Create the re-export shims at the old core paths.** `packages/core/src/types/constants.ts`: - -```ts -// Moved to @modelcontextprotocol/sdk-shared. Re-exported here so core's internal -// relative imports (./constants.js) keep resolving without a wide rename. -export * from '@modelcontextprotocol/sdk-shared'; -``` - -`packages/core/src/types/schemas.ts`: - -```ts -// Moved to @modelcontextprotocol/sdk-shared. -export * from '@modelcontextprotocol/sdk-shared'; -``` - -`packages/core/src/types/types.ts`: - -```ts -// Moved to @modelcontextprotocol/sdk-shared (types-only subpath keeps this Zod-free). -export * from '@modelcontextprotocol/sdk-shared/types'; -``` - -(The `schemas.ts` shim re-exports the full surface so `import * as schemas from './schemas.js'` in `specTypeSchema.ts` still finds every `*Schema` value. The `types.ts` shim uses the types-only subpath so anything `export *`-ing it stays Zod-free.) - -- [ ] **Step 3: Re-point the types `export *` in `packages/core/src/exports/public/index.ts`.** The line currently reads `export * from '../../types/types.js';`. It can stay as-is (the shim now forwards to `sdk-shared/types`). **Verify** the constants named-export block (`export { BAGGAGE_META_KEY, … } from '../../types/constants.js';`) still resolves through the `constants.ts` shim. No code change required if shims are in place — confirm in Step 5. - -- [ ] **Step 4: Update `core`'s tsgo path mapping if needed.** If Step 5 typecheck fails to resolve `@modelcontextprotocol/sdk-shared`, add to `packages/core/tsconfig.json` `compilerOptions.paths`: - -```json -"@modelcontextprotocol/sdk-shared": ["./node_modules/@modelcontextprotocol/sdk-shared/src/index.ts"], -"@modelcontextprotocol/sdk-shared/types": ["./node_modules/@modelcontextprotocol/sdk-shared/src/types.ts"] -``` - -- [ ] **Step 5: Reinstall, typecheck, and test core** - -Run: `pnpm install && pnpm --filter @modelcontextprotocol/core typecheck && pnpm --filter @modelcontextprotocol/core test` -Expected: typecheck PASS; all core tests PASS. The key assertion: `specTypeSchemas` still builds (it reads schema values through the `schemas.ts` shim). - -- [ ] **Step 6: Commit** (print for the user) - -```bash -git add packages/core -git commit -m "refactor(core): consume sdk-shared via re-export shims; keep public surface unchanged" -``` - ---- - -### Task 1.4: Wire `server` and `client` to depend on `sdk-shared` (external, not bundled) - -**Files:** -- Modify: `packages/server/package.json`, `packages/client/package.json` (add dependency) -- Modify: `packages/server/tsdown.config.ts`, `packages/client/tsdown.config.ts` (external + dts paths) -- Modify: `packages/server/tsconfig.json`, `packages/client/tsconfig.json` (tsgo path mapping) - -**Interfaces:** -- Consumes: `@modelcontextprotocol/sdk-shared` at runtime (external dependency). -- Produces: `server`/`client` `dist` no longer inlines the schema/type source; their root barrels still re-export the spec **types** and `specTypeSchemas` (Zod-free); `barrelClean` still passes. - -- [ ] **Step 1: Add the dependency** to both `packages/server/package.json` and `packages/client/package.json` `dependencies`: - -```json -"@modelcontextprotocol/sdk-shared": "workspace:^" -``` - -- [ ] **Step 2: Mark it external in `packages/server/tsdown.config.ts`.** Add `'@modelcontextprotocol/sdk-shared'` to the `external` array (create the array if absent — server already has `external: ['@modelcontextprotocol/server/_shims']`): - -```ts - external: ['@modelcontextprotocol/server/_shims', '@modelcontextprotocol/sdk-shared'], -``` - -Add its source path to the dts `compilerOptions.paths` block so `.d.mts` generation resolves the external types: - -```ts - '@modelcontextprotocol/sdk-shared': ['../sdk-shared/src/index.ts'], - '@modelcontextprotocol/sdk-shared/types': ['../sdk-shared/src/types.ts'], -``` - -- [ ] **Step 3: Do the same in `packages/client/tsdown.config.ts`** (add `'@modelcontextprotocol/sdk-shared'` to `external`, and the two `paths` entries to the dts block). - -- [ ] **Step 4: Add tsgo path mapping** to `packages/server/tsconfig.json` and `packages/client/tsconfig.json` `compilerOptions.paths` (mirroring how they map `@modelcontextprotocol/core`): - -```json -"@modelcontextprotocol/sdk-shared": ["./node_modules/@modelcontextprotocol/sdk-shared/src/index.ts"], -"@modelcontextprotocol/sdk-shared/types": ["./node_modules/@modelcontextprotocol/sdk-shared/src/types.ts"] -``` - -- [ ] **Step 5: Reinstall, build, typecheck, test both packages** - -Run: `pnpm install && pnpm --filter @modelcontextprotocol/server --filter @modelcontextprotocol/client build && pnpm --filter @modelcontextprotocol/server --filter @modelcontextprotocol/client typecheck && pnpm --filter @modelcontextprotocol/server --filter @modelcontextprotocol/client test` -Expected: all PASS, including `barrelClean.test.ts`. - -- [ ] **Step 6: Verify `sdk-shared` is external in the build output** (not inlined) - -Run: `grep -c "@modelcontextprotocol/sdk-shared" packages/server/dist/index.mjs` -Expected: ≥ 1 (an `import ... from "@modelcontextprotocol/sdk-shared..."` line — proving it's referenced as an external dependency, not bundled). And the spec schema source is NOT inlined: `grep -c "z.object" packages/server/dist/index.mjs` should be markedly lower than before the change (spot-check; not a hard gate). - -- [ ] **Step 7: Full repo gate** - -Run: `pnpm typecheck:all && pnpm test:all` -Expected: all PASS. This confirms the move didn't break any sibling package. - -- [ ] **Step 8: Commit** (print for the user) - -```bash -git add packages/server packages/client -git commit -m "refactor(server,client): depend on sdk-shared as an external dependency" -``` - ---- - -## Phase 2 — Codemod: route `types.js` → `sdk-shared`, drop `specSchemaAccess` - -### Task 2.1: Register `sdk-shared` in the codemod version map - -**Files:** -- Modify: `packages/codemod/scripts/generateVersions.ts` -- Regenerate: `packages/codemod/src/generated/versions.ts` - -**Interfaces:** -- Produces: `V2_PACKAGE_VERSIONS` includes `@modelcontextprotocol/sdk-shared`, so `updatePackageJson` is allowed to add it to a consumer's deps. - -- [ ] **Step 1: Add `sdk-shared` to `PACKAGE_DIRS`** in `packages/codemod/scripts/generateVersions.ts`: - -```ts -const PACKAGE_DIRS: Record = { - '@modelcontextprotocol/client': 'client', - '@modelcontextprotocol/server': 'server', - '@modelcontextprotocol/node': 'middleware/node', - '@modelcontextprotocol/express': 'middleware/express', - '@modelcontextprotocol/server-legacy': 'server-legacy', - '@modelcontextprotocol/sdk-shared': 'sdk-shared' -}; -``` - -- [ ] **Step 2: Regenerate** - -Run: `pnpm --filter @modelcontextprotocol/codemod generate:versions` -Expected: `src/generated/versions.ts` now contains `'@modelcontextprotocol/sdk-shared': '^2.0.0-alpha.2'`. Verify: `grep sdk-shared packages/codemod/src/generated/versions.ts`. - -- [ ] **Step 3: Commit** (print for the user) - -```bash -git add packages/codemod/scripts/generateVersions.ts packages/codemod/src/generated/versions.ts -git commit -m "feat(codemod): register sdk-shared in V2_PACKAGE_VERSIONS" -``` - ---- - -### Task 2.2: Route `sdk/types.js` to `sdk-shared` (TDD) - -**Files:** -- Test: `packages/codemod/test/v1-to-v2/transforms/importPaths.test.ts` -- Modify: `packages/codemod/src/migrations/v1-to-v2/mappings/importMap.ts` - -**Interfaces:** -- Consumes: `lookupImportMapping` (already extension-tolerant from prior work). -- Produces: any import from `@modelcontextprotocol/sdk/types.js` or `@modelcontextprotocol/sdk/types` is rewritten to `@modelcontextprotocol/sdk-shared` (fixed target, no context resolution), names preserved, and `@modelcontextprotocol/sdk-shared` is added to `usedPackages`. - -- [ ] **Step 1: Write the failing test** — add to `importPaths.test.ts` inside the `describe('import-paths transform', …)` block. Also covers that schema-value imports keep their names (no `specTypeSchemas` rewrite): - -```ts -it('routes sdk/types.js to @modelcontextprotocol/sdk-shared (types + schemas, fixed target)', () => { - const input = [ - `import { CallToolResult, CallToolResultSchema } from '@modelcontextprotocol/sdk/types.js';`, - '' - ].join('\n'); - const project = new Project({ useInMemoryFileSystem: true }); - const sourceFile = project.createSourceFile('test.ts', input); - const result = importPathsTransform.apply(sourceFile, { projectType: 'server' }); - const output = sourceFile.getFullText(); - expect(output).toContain(`from "@modelcontextprotocol/sdk-shared"`); - expect(output).toContain('CallToolResult'); - expect(output).toContain('CallToolResultSchema'); - expect(output).not.toContain('@modelcontextprotocol/sdk/types'); - expect(output).not.toContain('specTypeSchemas'); - expect(result.usedPackages?.has('@modelcontextprotocol/sdk-shared')).toBe(true); -}); -``` - -- [ ] **Step 2: Run it; verify it fails** - -Run: `pnpm --filter @modelcontextprotocol/codemod test -- importPaths -t "sdk-shared (types + schemas"` -Expected: FAIL — current output routes to `@modelcontextprotocol/server` (RESOLVE_BY_CONTEXT), so the `sdk-shared` assertion fails. - -- [ ] **Step 3: Change the `types.js` mapping** in `packages/codemod/src/migrations/v1-to-v2/mappings/importMap.ts`. Replace the existing entry: - -```ts - '@modelcontextprotocol/sdk/types.js': { - target: '@modelcontextprotocol/sdk-shared', - status: 'moved', - renamedSymbols: { - ResourceTemplate: 'ResourceTemplateType' - } - }, -``` - -(Only this entry changes from `RESOLVE_BY_CONTEXT` to the fixed `@modelcontextprotocol/sdk-shared` target. Leave `shared/protocol.js`, `shared/transport.js`, `inMemory.js`, etc. as `RESOLVE_BY_CONTEXT`.) - -- [ ] **Step 4: Run the test; verify it passes** - -Run: `pnpm --filter @modelcontextprotocol/codemod test -- importPaths -t "sdk-shared (types + schemas"` -Expected: PASS. - -- [ ] **Step 5: Update the now-obsolete `types.js` context tests.** The existing tests `resolves sdk/types.js based on sibling client imports`, `resolves sdk/types.js based on sibling server imports`, and the extensionless `resolves extensionless sdk/types …` tests now expect `@modelcontextprotocol/sdk-shared` instead of `@modelcontextprotocol/client`/`/server`. Update each assertion to `expect(result).toContain('@modelcontextprotocol/sdk-shared')` (drop the client/server expectations for the `types`-only cases). Re-run the full file: - -Run: `pnpm --filter @modelcontextprotocol/codemod test -- importPaths` -Expected: all PASS. - -- [ ] **Step 6: Commit** (print for the user) - -```bash -git add packages/codemod/src/migrations/v1-to-v2/mappings/importMap.ts packages/codemod/test/v1-to-v2/transforms/importPaths.test.ts -git commit -m "feat(codemod): route sdk/types.js to @modelcontextprotocol/sdk-shared" -``` - ---- - -### Task 2.3: Remove the `specSchemaAccess` transform - -**Files:** -- Modify: `packages/codemod/src/migrations/v1-to-v2/transforms/index.ts` -- Delete: `packages/codemod/src/migrations/v1-to-v2/transforms/specSchemaAccess.ts` -- Delete: `packages/codemod/test/v1-to-v2/transforms/specSchemaAccess.test.ts` -- Modify: codemod integration tests that assert `specTypeSchemas` output (e.g. `test/integration.test.ts`) - -**Interfaces:** -- Produces: `*Schema` value usages (`.parse`, `.safeParse`, `.extend`, …) pass through untouched — they ride the `types.js → sdk-shared` path swap with names intact. - -- [ ] **Step 1: Write a failing pass-through test** in `importPaths.test.ts` (or a new `test/v1-to-v2/passthrough.test.ts` running the full migration) asserting `.parse()` survives. Minimal transform-level version: - -```ts -it('leaves *Schema runtime usage (.parse) untouched after routing to sdk-shared', () => { - const input = [ - `import { CallToolResultSchema } from '@modelcontextprotocol/sdk/types.js';`, - `const x = CallToolResultSchema.parse(value);`, - '' - ].join('\n'); - const project = new Project({ useInMemoryFileSystem: true }); - const sourceFile = project.createSourceFile('test.ts', input); - importPathsTransform.apply(sourceFile, { projectType: 'server' }); - const output = sourceFile.getFullText(); - expect(output).toContain('CallToolResultSchema.parse(value)'); - expect(output).not.toContain('specTypeSchemas'); - expect(output).not.toContain("['~standard']"); -}); -``` - -- [ ] **Step 2: Run it; verify current behavior** — with `specSchemaAccess` still in the pipeline this transform-only test on `importPathsTransform` already passes (specSchemaAccess is a separate transform). To see the regression the removal prevents, run the FULL migration in this test instead by importing and applying every transform in order. Confirm that BEFORE removal the full-migration output contains `specTypeSchemas` (FAIL of the `not.toContain` assertion), proving `specSchemaAccess` is what rewrites it. - -Run: `pnpm --filter @modelcontextprotocol/codemod test -- passthrough` -Expected: FAIL on `not.toContain('specTypeSchemas')` (full-migration variant). - -- [ ] **Step 3: Remove `specSchemaAccess` from the pipeline** in `packages/codemod/src/migrations/v1-to-v2/transforms/index.ts` — delete its import and its entry in the exported transforms array. - -- [ ] **Step 4: Delete the transform + its unit test** - -```bash -git rm packages/codemod/src/migrations/v1-to-v2/transforms/specSchemaAccess.ts -git rm packages/codemod/test/v1-to-v2/transforms/specSchemaAccess.test.ts -``` - -- [ ] **Step 5: Update integration tests.** Search for residual expectations and fix them: - -Run: `grep -rn "specTypeSchemas\|~standard\|specSchemaAccess" packages/codemod/test packages/codemod/src/migrations` -Expected after fixes: only legitimate references remain (none asserting the codemod *produces* `specTypeSchemas`). Update `test/integration.test.ts` cases that expected `.parse`→`validate` rewrites to instead expect the schema usage unchanged. - -- [ ] **Step 6: Run the full codemod suite** - -Run: `pnpm --filter @modelcontextprotocol/codemod test` -Expected: all PASS. - -- [ ] **Step 7: Typecheck + lint the codemod** (catches the dangling `specSchemaAccess` import and any unused `specSchemaMap` reference) - -Run: `pnpm --filter @modelcontextprotocol/codemod check` -Expected: PASS. If `src/generated/specSchemaMap.ts` / `scripts/generateSpecSchemaMap.ts` are now unused, remove them and the `generate:spec-schemas` prebuild step; otherwise leave them. - -- [ ] **Step 8: Commit** (print for the user) - -```bash -git add packages/codemod -git commit -m "refactor(codemod): drop specSchemaAccess; schema usage migrates by path swap" -``` - ---- - -## Phase 3 — Batch-test validation + docs - -### Task 3.1: Teach the batch test about `sdk-shared` and re-validate firebase-tools - -**Files:** -- Modify: `packages/codemod/src/bin/batchTest.ts` - -**Interfaces:** -- Consumes: the packed local tarballs. -- Produces: the batch test packs `sdk-shared` and forces the transitive `server`/`client` → `sdk-shared` edge to resolve to the local tarball. - -- [ ] **Step 1: Add `sdk-shared` to `LOCAL_PACKAGE_DIRS`** in `packages/codemod/src/bin/batchTest.ts`: - -```ts - '@modelcontextprotocol/sdk-shared': path.join(SDK_ROOT, 'packages/sdk-shared'), -``` - -- [ ] **Step 2: Force transitive resolution via `overrides`.** In `rewriteToLocalTarballs` (or right after it), ensure the consumer `package.json` gets an `overrides` map pinning `@modelcontextprotocol/sdk-shared` (and the other v2 packages) to their local tarball paths, so `server`'s own `^2.0.0-alpha.2` dependency on `sdk-shared` resolves locally. Add, after the dependency rewrite loop: - -```ts - // npm/pnpm: pin transitive @modelcontextprotocol/* (e.g. server -> sdk-shared) to local tarballs. - const overrides = (pkgJson.overrides as Record | undefined) ?? {}; - for (const [name, tarballPath] of Object.entries(tarballs)) { - overrides[name] = `file:${tarballPath}`; - } - pkgJson.overrides = overrides; - rewrites++; // ensure the file is written -``` - -(If the manifest's package manager is pnpm, the equivalent key is `pnpm.overrides`; firebase-tools uses npm, so top-level `overrides` is correct. Generalize only if a pnpm repo is added.) - -- [ ] **Step 3: Rebuild SDK packages and re-run the batch test** - -Run: `pnpm build:all && pnpm --filter @modelcontextprotocol/codemod batch-test` -Expected: completes; `packages/codemod/batch-test/results/summary.json` shows `firebase/firebase-tools` with `newErrors.typecheck: 0`. - -- [ ] **Step 4: Confirm the win in the report** - -Run: `node -e "const r=require('./packages/codemod/batch-test/results/firebase_firebase-tools/report.json');const p=r.packages[0];console.log('post typecheck exit:',p.postCodemod.typecheck.exitCode);console.log('Unknown SDK import path diags:',p.codemod.diagnostics.filter(d=>d.message.includes('Unknown SDK import path')).length);console.log('project-type diags:',p.codemod.diagnostics.filter(d=>d.message.includes('Could not determine project type')).length);"` -Expected: `post typecheck exit: 0`; `Unknown SDK import path diags: 0`; `project-type diags: 0`. Spot-check `repos/firebase_firebase-tools/src/mcp/onemcp/onemcp_server.ts` — the `.parse()` calls are intact and import `*Schema` from `@modelcontextprotocol/sdk-shared`. - -- [ ] **Step 5: Commit** (print for the user) - -```bash -git add packages/codemod/src/bin/batchTest.ts -git commit -m "test(codemod): pack sdk-shared and pin transitive deps in batch test" -``` - ---- - -### Task 3.2: Migration docs + finalize - -**Files:** -- Modify: `docs/migration.md`, `docs/migration-SKILL.md` - -**Interfaces:** none (docs only). - -- [ ] **Step 1: Rewrite the spec-schema validation section in `docs/migration.md`.** Replace the `CallToolResultSchema` → `specTypeSchemas.X['~standard'].validate()` guidance (around the section found by `grep -n "specTypeSchemas\|CallToolResultSchema" docs/migration.md`) with: - -```md -### Schema validation (`*Schema.parse` / `.safeParse`) - -The Zod schema constants moved to `@modelcontextprotocol/sdk-shared`. Update the import path; the schemas are unchanged Zod schemas, so `.parse()`, `.safeParse()`, `.extend()`, etc. keep working. - -```ts -// v1 -import { CallToolResultSchema } from '@modelcontextprotocol/sdk/types.js'; -// v2 -import { CallToolResultSchema } from '@modelcontextprotocol/sdk-shared'; - -const result = CallToolResultSchema.parse(value); // unchanged -``` - -For library-agnostic (Standard Schema) validation that does not couple your code to Zod, use `specTypeSchemas` from `@modelcontextprotocol/server` or `@modelcontextprotocol/client` instead: - -```ts -import { specTypeSchemas } from '@modelcontextprotocol/server'; -const r = specTypeSchemas.CallToolResult['~standard'].validate(value); // { value, issues } -``` -``` - -- [ ] **Step 2: Update `docs/migration-SKILL.md`** — replace the mapping-table rows that map `Schema.parse(value)` → `specTypeSchemas.['~standard'].validate(value)` with a row mapping the **import path**: `import … from '@modelcontextprotocol/sdk/types.js'` → `import … from '@modelcontextprotocol/sdk-shared'` (schemas and types), and note that `.parse`/`.safeParse` are unchanged. Keep the `specTypeSchemas` row as the optional library-agnostic alternative. - -- [ ] **Step 3: Sync snippets + docs check** - -Run: `pnpm sync:snippets && pnpm run docs:check` -Expected: PASS (or no changes). Fix any snippet drift. - -- [ ] **Step 4: Final full-repo gate** - -Run: `pnpm check:all && pnpm test:all` -Expected: all PASS. - -- [ ] **Step 5: Commit** (print for the user) - -```bash -git add docs/migration.md docs/migration-SKILL.md -git commit -m "docs(migration): schemas import from @modelcontextprotocol/sdk-shared" -``` - ---- - -## Self-Review - -**Spec coverage:** package creation (1.1), spec types + Zod schema move (1.2), first-class Zod positioning / no nudge (codemod has no nudge; docs present both — 2.2/2.3/3.2), regular `dependency` model (1.3/1.4), external-not-bundled (1.4), types-only re-export keeping the surface Zod-free (1.2 `./types` + 1.3 shim), `specTypeSchemas` unchanged (1.3), churn-limiting shims (1.3), codemod path swap (2.2), drop `specSchemaAccess` (2.3), batch-test wiring + 0-error validation (3.1), docs + changeset (1.1/3.2). PR #2277 supersession is covered by the `specSchemaAccess` removal (no `specTypeSchemas` rewrite produced). All spec sections map to a task. - -**Placeholder scan:** no `TBD`/`TODO`; the one conditional (`tsconfig paths` in 1.3 Step 4 / 1.4 Step 4) is gated on a concrete typecheck failure with the exact lines to add. Move tasks specify exact `git mv` targets rather than reproducing the 2346-line `schemas.ts` (relocation, not authoring). - -**Type/name consistency:** `@modelcontextprotocol/sdk-shared` used verbatim throughout; `./types` subpath defined in 1.1 (package.json exports + typesVersions), produced in 1.2 (built from `src/types.ts`), consumed in 1.3 (core `types.ts` shim) and 1.4 (server/client dts paths); `lookupImportMapping` (2.2) matches the existing helper; `LOCAL_PACKAGE_DIRS`/`rewriteToLocalTarballs`/`tarballs` (3.1) match `batchTest.ts`. - -## Execution Handoff - -Two execution options: - -1. **Subagent-Driven (recommended)** — a fresh subagent per task, with review between tasks. -2. **Inline Execution** — execute tasks in this session with checkpoints. - -Note: every "Commit" step prints commands for **you** to run (the `git add`/`git commit` hook blocks the agent from committing). diff --git a/docs/superpowers/specs/2026-05-11-codemod-batch-test-design.md b/docs/superpowers/specs/2026-05-11-codemod-batch-test-design.md deleted file mode 100644 index 03709e94e2..0000000000 --- a/docs/superpowers/specs/2026-05-11-codemod-batch-test-design.md +++ /dev/null @@ -1,288 +0,0 @@ -# Codemod Batch Test: Design Spec - -Repeatable process for running the MCP v1-to-v2 codemod against real-world repos, identifying issues, and iterating on the codemod. - -## Goal - -Improve the codemod by testing it against 10-15 curated external repos. Each iteration: run the codemod, compare baseline vs. post-codemod check results, have Claude categorize failures, fix the codemod, repeat. - -## System Overview - -Three components, all living in `packages/codemod/batch-test/`: - -1. **Repo manifest** (`repos.json`) -- JSON file listing target repos, their structure, and optional overrides. -2. **Batch runner** (`run-codemod-batch.sh`) -- Shell script that iterates the manifest: clones, installs, baselines, codemods, re-checks, writes structured output. -3. **Analysis prompt** (`analyze-prompt.md`) -- Instructions for Claude Code to run the script and analyze results in a single session. - -### Data Flow - -``` -repos.json --> run-codemod-batch.sh --> results//report.json (per-repo) - --> results/summary.json (consolidated) - -Claude Code: runs script, reads results, produces categorized analysis -``` - -## Repo Manifest (`repos.json`) - -An array of repo entries. Each entry represents a GitHub repo and one or more packages within it that use `@modelcontextprotocol/sdk` v1. - -```json -[ - { - "repo": "owner/repo-name", - "ref": "main", - "packages": [ - { - "dir": "packages/mcp-server", - "sourceDir": "src", - "checks": { - "typecheck": "npm run check:ts", - "test": null - } - } - ] - } -] -``` - -### Fields - -| Field | Required | Default | Description | -|-------|----------|---------|-------------| -| `repo` | yes | -- | GitHub `owner/name` | -| `ref` | no | `main` | Branch or tag to check out | -| `packages` | no | `[{ "dir": ".", "sourceDir": "src" }]` | Package targets within the repo | -| `packages[].dir` | yes | -- | Path to the package root (where its `package.json` lives) | -| `packages[].sourceDir` | no | `src` | Source directory relative to `dir` (passed to codemod) | -| `packages[].checks` | no | auto-detect | Override check commands; set a key to `null` to skip that check | - -### Auto-Detection Rules - -**Package manager** (first lockfile found at repo root): -- `pnpm-lock.yaml` -> `pnpm` -- `yarn.lock` -> `yarn` -- `package-lock.json` -> `npm` -- `bun.lockb` -> `bun` - -**Check commands** (read `scripts` from the package's `package.json`, first match wins): - -| Check | Script names probed (in order) | Fallback | -|-------|-------------------------------|----------| -| typecheck | `typecheck`, `type-check`, `check:types`, `tsc` | `npx tsc --noEmit` | -| build | `build`, `compile` | skip | -| test | `test`, `test:unit`, `test:all` | skip | -| lint | `lint`, `lint:check` | skip | - -The detected command runs as ` run `. - -## Batch Runner (`run-codemod-batch.sh`) - -### CLI - -```bash -./run-codemod-batch.sh [--manifest repos.json] [--output-dir ./results] [--clone-dir ./repos] [--fresh-clones] -``` - -| Flag | Default | Description | -|------|---------|-------------| -| `--manifest` | `./repos.json` | Path to repo manifest | -| `--output-dir` | `./results` | Where to write reports | -| `--clone-dir` | `./repos` | Where to clone repos | -| `--fresh-clones` | off | Force re-clone even if clone exists | - -Clones are kept between runs by default for fast iteration. - -### Per-Repo Flow - -``` -1. CLONE OR RESET - - If clone exists: git restore . && git clean -fd - - If no clone: git clone --depth 1 --branch - -2. DETECT PACKAGE MANAGER - - Check for lockfile at repo root - -3. INSTALL - - cd && install - - If install fails: record error, skip to next repo - -4. BASELINE CHECKS (for each package) - - Auto-detect or use override check commands - - Run: typecheck, build, test, lint - - Capture: exit code, stdout, stderr for each - -5. RUN CODEMOD (for each package) - - node /packages/codemod/dist/cli.mjs v1-to-v2 \ - // --verbose - - Capture: full output, diagnostics, change count - -6. RE-INSTALL - - cd && install - - Picks up new v2 deps from updated package.json files - -7. POST-CODEMOD CHECKS (for each package) - - Same checks as step 4, captured separately - -8. WRITE REPORT - - Write per-repo JSON to results//report.json - - Append entry to summary -``` - -### Error Handling - -If any step fails for a repo, the script logs the failure, writes what it has to the report, and moves to the next repo. One broken repo does not stop the batch. - -### Path Resolution - -The script resolves `SDK_ROOT` from its own location (`SDK_ROOT=$(cd "$(dirname "$0")/../../.." && pwd)`). All default paths (`--clone-dir`, `--output-dir`) are relative to the script's directory (`packages/codemod/batch-test/`). - -### Codemod Binary - -The script always uses the locally-built codemod from the current branch: -``` -node "$SDK_ROOT/packages/codemod/dist/cli.mjs" -``` -This ensures each run tests the current state of the codemod. - -## Output Format - -### Per-Repo Report (`results//report.json`) - -```json -{ - "repo": "user/mcp-server-example", - "ref": "main", - "timestamp": "2026-05-11T14:30:00Z", - "packageManager": "pnpm", - "packages": [ - { - "dir": ".", - "sourceDir": "src", - "codemod": { - "filesChanged": 12, - "totalChanges": 47, - "diagnostics": [ - { - "level": "warning", - "file": "src/server.ts", - "line": 42, - "message": "Destructuring pattern for 'extra' -- review manually", - "transformId": "context" - } - ] - }, - "baseline": { - "typecheck": { "exitCode": 0, "stdout": "", "stderr": "" }, - "build": { "exitCode": 0, "stdout": "", "stderr": "" }, - "test": { "exitCode": 0, "stdout": "", "stderr": "" }, - "lint": { "exitCode": 0, "stdout": "", "stderr": "" } - }, - "postCodemod": { - "typecheck": { "exitCode": 2, "stdout": "", "stderr": "src/handler.ts(15,3): error TS2345: ..." }, - "build": { "exitCode": 2, "stdout": "", "stderr": "..." }, - "test": { "exitCode": 0, "stdout": "", "stderr": "" }, - "lint": { "exitCode": 0, "stdout": "", "stderr": "" } - } - } - ] -} -``` - -### Consolidated Summary (`results/summary.json`) - -```json -{ - "timestamp": "2026-05-11T14:30:00Z", - "codemodVersion": "2.0.0-alpha.0", - "codemodCommit": "abc1234", - "totalRepos": 12, - "totalPackages": 15, - "results": [ - { - "repo": "user/mcp-server-example", - "package": ".", - "baselineClean": true, - "postCodemodClean": false, - "newErrors": { "typecheck": 3, "build": 1, "test": 0, "lint": 0 }, - "codemodDiagnostics": { "warning": 2, "error": 0, "info": 1 } - } - ], - "aggregated": { - "reposClean": 7, - "reposWithNewErrors": 5, - "totalNewTypecheckErrors": 18, - "totalCodemodWarnings": 12, - "topErrorPatterns": ["TS2345", "TS2339", "TS2554"] - } -} -``` - -## Claude Analysis Workflow - -### Prompt (`analyze-prompt.md`) - -Saved in `packages/codemod/batch-test/analyze-prompt.md`. You tell Claude Code to follow these instructions: - -``` -Run the batch codemod test and analyze results: - -1. Build the codemod: - pnpm --filter @modelcontextprotocol/codemod build - -2. Run the batch test: - ./packages/codemod/batch-test/run-codemod-batch.sh - -3. Read results/summary.json for the overview. - -4. For each repo with new errors, read its results//report.json. - -5. Categorize each new error (present in postCodemod but not in baseline): - - codemod-bug: The transform produced incorrect output - - missing-transform: The codemod should handle this pattern but doesn't - - manual-migration: Expected -- documented in migration guide, needs human judgment - - repo-specific: Unusual pattern unique to this repo, not worth handling - -6. Produce findings grouped by category with: - - Repo, file, line, error message - - Root cause (one sentence) - - For codemod-bug/missing-transform: which transform to fix and what correct output looks like - -7. Produce a "Priority Fixes" list: top 3-5 codemod improvements sorted by impact - (number of repos affected). -``` - -### Iteration Loop - -``` -1. Fix a codemod transform -2. Tell Claude: "Re-run the batch test and analyze" - --> Claude rebuilds codemod, resets clones, re-runs, reads results, analyzes -3. Review Claude's findings -4. Go to 1 -``` - -## Error Categorization Reference - -| Category | Meaning | Action | -|----------|---------|--------| -| `codemod-bug` | Transform produced wrong output | Fix the transform | -| `missing-transform` | Pattern not handled | Add handling to existing transform or create new one | -| `manual-migration` | Requires human judgment (removed API, architectural change) | Ensure migration guide covers it; improve codemod diagnostic | -| `repo-specific` | Unusual pattern unique to one repo | Document but don't add to codemod | - -## File Structure - -``` -packages/codemod/batch-test/ - repos.json # Repo manifest (curated list) - run-codemod-batch.sh # Batch runner script - analyze-prompt.md # Claude analysis instructions - repos/ # Cloned repos (gitignored) - results/ # Output reports (gitignored) - summary.json - / - report.json -``` - -`repos/` and `results/` are added to `.gitignore`. Only the manifest, script, and prompt are committed. diff --git a/docs/superpowers/specs/2026-06-02-readbuffer-max-size-design.md b/docs/superpowers/specs/2026-06-02-readbuffer-max-size-design.md deleted file mode 100644 index a8e89c3647..0000000000 --- a/docs/superpowers/specs/2026-06-02-readbuffer-max-size-design.md +++ /dev/null @@ -1,61 +0,0 @@ -# ReadBuffer Maximum Size Guard - -**Date:** 2026-06-02 -**Advisory:** GHSA-wqgc-pwpr-pq7r -**Severity:** Low (DoS via stdio transport, local attack surface) - -## Problem - -`ReadBuffer.append()` in `packages/core/src/shared/stdio.ts` concatenates incoming data with no size limit. A malicious MCP server subprocess can write continuous data to stdout without newline delimiters, causing the host process (Claude Desktop, Cursor, VS Code, etc.) to grow memory without bound until OOM-killed. - -The `data` event handlers in both `StdioClientTransport` and `StdioServerTransport` call `append()` outside any try/catch, so a thrown error from `append()` would become an uncaught exception — this must also be addressed. - -## Design - -### 1. ReadBuffer (`packages/core/src/shared/stdio.ts`) - -- Add exported constant `DEFAULT_MAX_BUFFER_SIZE = 10 * 1024 * 1024` (10 MB). -- Constructor accepts optional `{ maxBufferSize?: number }` options object. -- `append()` checks `(currentSize + chunk.length) > maxBufferSize` before concatenating. -- On overflow: call `this.clear()` first (leave object in clean state), then throw `Error`. -- Fully backwards compatible — `new ReadBuffer()` with no args uses the default. - -### 2. StdioClientTransport (`packages/client/src/client/stdio.ts`) - -- Wrap the `stdout.on('data')` handler body in try/catch. -- On catch: route error to `this.onerror?.(error)`, then call `this.close()`. - -### 3. StdioServerTransport (`packages/server/src/server/stdio.ts`) - -- Wrap the `_ondata` handler body in try/catch. -- On catch: route error to `this.onerror?.(error)`, then call `this.close()`. - -### 4. Tests (`packages/core/test/shared/stdio.test.ts`) - -- `append()` throws when buffer exceeds default limit. -- `append()` throws with custom `maxBufferSize`. -- Buffer is cleared after overflow (object reusable). -- Default limit can be overridden via constructor. - -### 5. No changes to - -- Public API exports (`ReadBuffer` is already exported; constructor change is additive). -- `processReadBuffer()` in either transport (existing try/catch handles `readMessage()` errors; new try/catch handles `append()` errors at a higher level). - -## Files Modified - -| File | Change | -|------|--------| -| `packages/core/src/shared/stdio.ts` | Add `DEFAULT_MAX_BUFFER_SIZE`, constructor options, size guard in `append()` | -| `packages/client/src/client/stdio.ts` | try/catch in `data` handler, close on overflow | -| `packages/server/src/server/stdio.ts` | try/catch in `_ondata` handler, close on overflow | -| `packages/core/test/shared/stdio.test.ts` | New tests for buffer overflow behavior | - -## Decision Log - -- **10 MB default** chosen because a single JSON-RPC message shouldn't realistically exceed a few MB (even a 7 MB binary base64-encoded is ~9.3 MB). Users with legitimate large messages can raise the cap explicitly. -- **Throw from append()** rather than silent truncation or callback — uses existing error propagation paths and makes the failure visible. -- **Clear before throw** so the ReadBuffer isn't left in a corrupt state. -- **Close transport on overflow** because a buffer overflow means the peer is misbehaving and any partial data is unrecoverable. -- **No chunk-list optimization** — the 10 MB cap bounds the `Buffer.concat()` amplification to ~50 MB worst case, which is acceptable. Chunk-list can be a separate follow-up. -- **Options object** (not bare number) for the constructor parameter, for future extensibility. diff --git a/docs/superpowers/specs/2026-06-08-sep-2549-ttl-design.md b/docs/superpowers/specs/2026-06-08-sep-2549-ttl-design.md deleted file mode 100644 index f59499ed9c..0000000000 --- a/docs/superpowers/specs/2026-06-08-sep-2549-ttl-design.md +++ /dev/null @@ -1,495 +0,0 @@ -# SEP-2549: TTL for List Results — Design - -**Status:** Draft for review (rev 2 — incorporates backend + software architecture review) -**Date:** 2026-06-08 -**SEP:** [2549 — TTL for List Results](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/docs/seps/2549-TTL-for-list-results.mdx) -**Branch:** `feature/v2-SEP-2549-ttl-for-list-results` - -## Context - -MCP clients currently discover changes to a server's tools/prompts/resources only via `list_changed` notifications, which require a long-lived SSE stream. Many HTTP clients and servers cannot reliably hold such streams, and there is no signal for how often a list actually changes. SEP-2549 adds two freshness fields — `ttlMs` and `cacheScope` — to the five cacheable result types so a server can tell clients how long a response stays fresh and who may cache it. This works alongside notifications (not as a replacement) and is fully backward-compatible with pre-2549 servers. - -The upstream draft spec (`schema/draft/schema.ts`) already defines these types. This work brings the TypeScript SDK to parity and — critically — makes the SDK actually *use* the hints: a client SDK that only emits the wire fields satisfies nothing of value, because `client.listTools()` would still refetch every time and discard the TTL. The deliverable therefore spans the wire types, server emission, a client-side cache, polling helpers, and a shared (multi-tenant) cache. - -### Scope decision - -Full implementation, all acceptance criteria. Confirmed during brainstorming: -- **Layer 5 (shared multi-tenant cache, R-2549-7): in scope** — ship it now. -- **Client cache enablement: `ClientOptions` flag** — cache logic lives in `Client`, off by default for backward compatibility. - -### Delivery: two PRs - -This design is implemented and reviewed as **two independent PRs**, because the API surface and risk profiles are very different: - -- **PR 1 — wire + server emission (Layers 1–2).** Low-risk, independently valuable, satisfies R-2549-1/3/8/12/13. Adds the spec-parity types and McpServer emission config. Ships first. -- **PR 2 — client cache, polling, shared store (Layers 3–5).** Carries all the contested surface (read-through caching, invalidation, multi-tenant isolation, polling). Built on top of PR 1, reviewed on its own. - -Both PRs are described here as one design for coherence; the File Summary marks which PR each file belongs to. - ---- - -## The two safety invariants - -Two invariants are load-bearing for the entire feature and every layer below depends on them. They are stated once here and tested explicitly. - -> **Invariant A — `ttlMs: 0` is never cached.** -> An entry with `ttlMs === 0` is never stored, never returned as fresh, and never shared across principals. This is what makes the feature backward-compatible (pre-2549 servers normalize to `ttlMs: 0` ⇒ behave exactly like today) **and** what makes a `cacheScope: 'public'` *default* safe at the wire layer (a defaulted-public entry with `ttlMs: 0` can never be served from any cache, shared or not). - -> **Invariant B — only *explicitly-declared* `cacheScope: 'public'` may be shared.** -> A shared cache (Layer 5) shares an entry across principals **only if the server explicitly sent `cacheScope: 'public'` on the wire.** An absent `cacheScope` — even though it normalizes to `'public'` for wire-type parity — is treated by the cache as *unknown ⇒ private ⇒ never shared.* This resolves the SEP's internal contradiction (see below) in the fail-safe direction: a misclassification costs at most a cache miss, never a cross-tenant data leak. - -### The SEP contradiction these invariants resolve - -The SEP is internally contradictory about the default for an absent `cacheScope`: - -- The `CacheableResult` JSDoc says: *"Defaults to `"public"` if absent."* -- The Backward Compatibility section says the opposite, and explains why: *"`cacheScope` is required because there is no safe default for older servers. The server must explicitly declare the intended cache scope to prevent unintended caching of user-specific data."* It calls out `resources/read` as user-specific (private). - -We honor **both** by separating two concerns the first draft conflated: - -1. **Wire-type normalization (Layer 1):** absent `cacheScope` → `'public'` *as a type-level default only*, so the SDK output type has the required field the spec demands (parity). This default value is never, by itself, an authorization to share — Invariant A guarantees a defaulted entry (which also has `ttlMs: 0` unless the server set a TTL) cannot be cached. -2. **Caching authorization (Layers 3 & 5):** the cache tracks whether `cacheScope` was *explicitly present on the wire* (`scopeExplicit`) and shares cross-principal only when it was explicitly `'public'` (Invariant B). - ---- - -## Acceptance Criteria → Layer Map - -| ID | Requirement | Layer | PR | -|----|-------------|-------|----| -| R-2549-1 (wire) | Server MUST include `ttlMs` (≥0) and `cacheScope` on the 5 result types | 1 + 2 | 1 | -| R-2549-3 (guidance) | Per-page `ttlMs`; freshness lives on the *result*, not on `Tool`/`Resource` | 1 | 1 | -| R-2549-12 | Absent `ttlMs` → treat as 0 (BC with pre-2549 servers) | 1 | 1 | -| R-2549-13 | Negative `ttlMs` → treat as 0 | 1 | 1 | -| R-2549-8 | Same `cacheScope` on every page of a paginated response | 2 | 1 | -| R-2549-2 | Client SHOULD refetch on next access after `ttlMs` expires; MAY serve stale on refetch error | 3 | 2 | -| R-2549-11 | `list_changed` / `resources/updated` invalidates the cache regardless of remaining TTL | 3 | 2 | -| R-2549-14 | Cursor invalid (`-32602` on next-page fetch) → discard cached pages, refetch from page 1 | 3 | 2 | -| R-2549-10 (sdk) | Polling helpers MUST apply jitter + backoff | 4 | 2 | -| R-2549-7 (security) | Shared caches MUST NOT serve `private` entries to a different user (key on auth principal) | 5 | 2 | -| R-2549-4 | `list_changed` notifications still delivered if subscribed — TTL is a hint, not a replacement | (asserted by test) | 2 | - -## Architecture Overview - -``` -┌─ Layer 1: Wire types (core/types) ──────────────────────────────────┐ -│ CacheableResult { ttlMs: number; cacheScope: 'public'|'private' } │ -│ → spread into 5 result schemas; .default() normalization only │ -│ → normalizeCacheable() helper does clamp/floor + records scopeExplicit│ -└───────────────────────────────────────────────────────────────────────┘ - │ emitted by │ consumed by - ▼ ▼ -┌─ Layer 2: Server (server/mcp) ─┐ ┌─ Layer 3: Client cache (client) ──┐ -│ McpServerOptions.cache (hints) │ │ ListCacheStore (pluggable) │ -│ injects ttlMs/cacheScope into │ │ + InMemoryListCacheStore (default)│ -│ list & read results │ │ freshness, invalidation, cursor │ -└─────────────────────────────────┘ └────────────────────────────────────┘ - │ used by │ impl - ▼ ▼ - ┌─ Layer 4: pollList ─┐ ┌─ Layer 5: SharedListCacheStore ┐ - │ jitter + backoff │ │ shares only explicit-public; │ - │ (opt-in) │ │ private namespaced by principal│ - └──────────────────────┘ └────────────────────────────────┘ -``` - ---- - -## Layer 1 — Wire Types - -**Files:** `packages/core/src/types/schemas.ts`, `packages/core/src/types/types.ts`, `packages/core/src/types/spec.types.ts` (regenerated), `packages/core/test/spec.types.test.ts`. Public export of the two new **types** rides the one sanctioned `export *` from `types.ts` (see *Type exports* below — this is the only wildcard; all package-barrel symbols are explicit). - -### Spec parity is the hard constraint - -`packages/core/test/spec.types.test.ts` enforces, for every type, **bidirectional assignability** (`sdk = spec; spec = sdk`) and **exact key parity** (`AssertExactKeys`), operating on `z.output` (`Infer`). The upstream spec defines: - -```typescript -export interface CacheableResult extends Result { - ttlMs: number; // REQUIRED - cacheScope: "public" | "private"; // REQUIRED -} -export interface ListToolsResult extends PaginatedResult, CacheableResult { tools: Tool[]; } -// ...ListPromptsResult, ListResourcesResult, ListResourceTemplatesResult likewise -export interface ReadResourceResult extends CacheableResult { contents: (...)[]; } -``` - -Because the spec fields are **required**, an `.optional()` Zod field would fail mutual assignability (an `sdk` value with `ttlMs?: number` is not assignable to a spec value requiring `ttlMs: number`). Therefore the SDK output type must have these fields **required**, while still tolerating their absence on the wire (R-2549-12). - -> **Parity covers `z.output` only.** The parity test never exercises `z.input`, so the "tolerates absence on the wire" property (R-2549-12/13) is **not** guarded by parity — it is guarded solely by the schema unit tests below. The doc previously implied parity validated normalization; it does not. - -### Mechanism: `.default()` only (no `.transform()`) - -The first draft used `.default().transform(...)`. We drop the transform for three reasons surfaced in review: (a) a `ZodEffects`/transform field is brittle when spread into objects that may later be `.extend()`/`.pick()`/`.partial()`ed; (b) it makes the normative clamp (negative→0, R-2549-13) invisible inside a field spread; (c) it widens the `z.input`/`z.output` skew more than necessary and runs again on any (future) outbound validation. Clamping moves into one named helper. - -```typescript -export const CacheScopeSchema = z.enum(['public', 'private']); - -// Field spread for the 5 result schemas. Plain .default() — output type is required, -// input is optional. No transform. -const cacheableResultFields = { - ttlMs: z.number().default(0), - cacheScope: CacheScopeSchema.default('public'), -}; -``` - -- `Infer` (z.output) = `{ ttlMs: number; cacheScope: 'public' | 'private' }` → **matches spec exactly** (parity passes). -- z.input allows both omitted → defaults applied at the parse boundary. The SDK validates results on the **receiving (client) side**, so absent `ttlMs` becomes `0` and absent `cacheScope` becomes `'public'` automatically. - -> **Spike the exact Zod v4 form before building Layer 3.** Confirm that `z.number().default(0)` spread into a `looseObject` result schema yields `z.output` with a *required* `number` and passes `AssertExactKeys` against the regenerated spec type. This is a 30-minute compile-only spike; do it first (it was the original Open Risk #1). - -### Normalization + the `scopeExplicit` signal - -Clamp/floor and the explicitness signal live in one helper consumed by the client cache (Layer 3). It runs on the **already-parsed** result *and* inspects the **raw** (pre-parse) payload to learn whether `cacheScope` was actually present on the wire: - -```typescript -// packages/core/src/types/... (exported for client use) -export interface NormalizedCacheMeta { - ttlMs: number; // clamped: negative/NaN/Infinity → 0, floored to int - cacheScope: CacheScope; // parsed value (defaulted to 'public' if absent) - scopeExplicit: boolean; // TRUE only if the raw wire payload contained `cacheScope` -} - -export function normalizeCacheMeta(parsed: CacheableResult, raw: unknown): NormalizedCacheMeta { - const ttlMs = Number.isFinite(parsed.ttlMs) && parsed.ttlMs > 0 ? Math.floor(parsed.ttlMs) : 0; // R-2549-12/13 - const scopeExplicit = typeof raw === 'object' && raw !== null && 'cacheScope' in raw; - return { ttlMs, cacheScope: parsed.cacheScope, scopeExplicit }; -} -``` - -This is the **single source of truth** for normalization. `scopeExplicit` is the signal Invariant B needs and that a bare `.default('public')` would otherwise erase. The client cache stores it on `CachedEntry`; `SharedListCacheStore` reads it to decide shareability. - -Spread `...cacheableResultFields` into the 5 result schemas only: `ListToolsResultSchema`, `ListPromptsResultSchema`, `ListResourcesResultSchema`, `ListResourceTemplatesResultSchema`, `ReadResourceResultSchema`. **Never** added to item schemas (`Tool`, `Resource`, etc.) — freshness lives on the result (R-2549-3). `ListRootsResult`, `PaginatedResult`, and `ResultSchema` are untouched. - -### Type exports & spec test - -- Add `CacheScope` and `CacheableResult` **type** exports in `types.ts`. These become public via the *one intentional* `export * from './types/types.js'` in `core/public` (documented there as the sanctioned wildcard). **All other new symbols** (`ListCacheStore`, `InMemoryListCacheStore`, `SharedListCacheStore`, `CacheHints`, `McpServerOptions`, `pollList`, etc.) live in the client/server packages and MUST be added as **explicit named exports** in `packages/client/src/index.ts` / `packages/server/src/index.ts` — never via `export *`. (The earlier draft's "transitively via `export *`" framing applied *only* to the two core types; do not let it leak into the package barrels.) -- The spec defines `CacheableResult` as a base interface with no standalone schema; the SDK mirrors it as an exported **type**, while the runtime schema is the `cacheableResultFields` spread. The exported type and the spread fields therefore have **no shared schema** and must be hand-kept in sync — call this out as a known manual-sync seam (it mirrors how `PaginatedResult` is handled today). -- `CacheableResult` is marked `@internal` in the upstream spec. We deliberately export it as public SDK API anyway, because it is the natural return-type contract for low-level handler authors (see ADR-001). Note the divergence in the export comment. -- Run `pnpm fetch:spec-types` first — the committed `spec.types.ts` is stale (commit `5c25208…`) and predates these fields. (`spec.types.ts` is `.gitignore`d and regenerated by `pnpm test`.) -- Add a `CacheableResult` entry to `sdkTypeChecks` and a `_K_CacheableResult` key-parity assertion in `spec.types.test.ts`. Update the expected spec-type count (`toHaveLength(176)` → new count). **Verify the `_meta`/index-signature interplay:** `ResultSchema` is a `looseObject` (carries an index signature), so confirm `AssertExactKeys` for `CacheableResult` resolves to exactly `{ ttlMs, cacheScope, _meta? }` and that the loose index signature neither makes the assertion trivially pass nor spuriously fail. - -### Breaking change — see ADR-001 - -Low-level `Server.setRequestHandler('tools/list', …)` handlers (and the other four) now have a return type requiring `ttlMs`/`cacheScope`. This is a **deliberate, recorded** breaking change, not an accident of typing. Rationale, the rejected alternative, and the blast radius are in **ADR-001** below. McpServer injects the fields so high-level authors are unaffected (Layer 2). Documented in `docs/migration.md` + `docs/migration-SKILL.md`. - ---- - -## ADR-001 — Breaking change to low-level `Server` handler return types - -**Status:** Accepted · **Context:** Layer 1/2 · **Decision owners:** SEP-2549 implementers - -**Context.** With `.default()`, the result schemas' `z.output` (= `Infer` = the public `ListToolsResult` type, and the type `ResultTypeMap['tools/list']` against which `setRequestHandler` types a handler's return value) has `ttlMs`/`cacheScope` as **required**. So every low-level handler for the 5 methods must now return both fields or fail to type-check. McpServer's own internal handlers are also consumers of this type and are updated in Layer 2. - -**The alternative considered.** Type handler returns against `z.input` (where `.default()` makes the fields optional) while keeping the public wire type as `z.output`. That would make the change *non-breaking* for low-level authors. - -**Why we reject it.** The protocol does **not** re-validate or re-parse outbound results through the result schema — outbound results are serialized as-is; only the *receiving* side parses (verified in `protocol.ts` request/response path). So `z.input`-typed handlers would put **no** `ttlMs`/`cacheScope` on the wire, silently violating R-2549-1 ("server MUST include"). Making the change non-breaking would require introducing (a) input/output result-type *duality* across `ResultTypeMap`/`InferHandlerResult` and (b) a new outbound-normalization pass that does not exist today — a larger, more invasive change than the break, and one that trades a compile-time error for a *silent* spec violation. - -**Decision.** Keep the breaking change. Surfacing the "server MUST include" obligation as a compile-time error on low-level handlers is the spec-faithful, fail-loud choice. - -**Consequences.** (1) Low-level `Server` users for the 5 methods must add the two fields — migration guide ships the exact two-field snippet and points to `normalizeCacheMeta`/`CacheableResult`. (2) McpServer is itself a consumer and is updated in the same PR. (3) If a future SEP needs non-breaking result-type evolution, the input/output duality is the path — recorded here so it isn't re-litigated. - ---- - -## Layer 2 — Server Emission - -**Files:** `packages/server/src/server/mcp.ts`, `packages/server/src/index.ts`. **(PR 1)** - -```typescript -// Renamed from the draft's `ListCacheConfig`: this is emission config (freshness hints), -// not a cache. Avoids prefix-collision with the client's ListCache* types. -export interface CacheHints { ttlMs?: number; cacheScope?: 'public' | 'private'; } - -export interface McpServerOptions extends ServerOptions { - cache?: { - tools?: CacheHints; - resources?: CacheHints; - resourceTemplates?: CacheHints; - prompts?: CacheHints; - resourceRead?: CacheHints; // hints for resources/read - }; -} -``` - -- `McpServer` constructor widens `options?: ServerOptions` → `McpServerOptions` (backward-compatible) and stores `_cacheOptions`. -- The 4 list handlers spread `{ ttlMs: cfg?.ttlMs ?? 0, cacheScope: cfg?.cacheScope ?? 'public' }` into their results. Because config is static per endpoint, **every page gets the same `cacheScope`** (R-2549-8) for free. -- `resources/read`: callback result is normalized — `ttlMs`/`cacheScope` from the callback win; otherwise fall back to `cache.resourceRead` config, then defaults. The `ReadResourceCallback` return type is loosened so callbacks may omit the fields; McpServer fills them. -- **`resources/read` privacy footgun (documented).** The SEP calls out `resources/read` as the user-specific endpoint. The emission default is `cacheScope: 'public'` only because the paired default `ttlMs: 0` makes it uncacheable (Invariant A). **The migration guide MUST warn:** if you configure a non-zero `ttlMs` on `resourceRead` (or return one from the callback) for user-specific content, you MUST also set `cacheScope: 'private'`, or a downstream shared gateway may cache it. As defense-in-depth, when `cache.resourceRead.ttlMs > 0` is configured but `cacheScope` is omitted, McpServer emits `'private'` (not `'public'`) for `resources/read` specifically. -- **R-2549-8 for low-level servers.** McpServer guarantees same-scope-per-page structurally (static config). A low-level `Server` author paginating by hand could still vary `cacheScope` across pages; this is the author's responsibility per the spec MUST. We document it; we do not add a runtime guard (the SDK does not own the low-level pagination loop). -- Export `CacheHints`, `McpServerOptions` from `@modelcontextprotocol/server` as **explicit named exports**. - ---- - -## Layer 3 — Client-Side List Cache - -**Files:** new `packages/client/src/client/listCache.ts`; modify `packages/client/src/client/client.ts`, `packages/client/src/index.ts`. **(PR 2)** - -### Pluggable store - -```typescript -export interface ListCacheKey { - method: 'tools/list' | 'prompts/list' | 'resources/list' | 'resources/templates/list' | 'resources/read'; - cursor?: string; // pagination position - uri?: string; // for resources/read - principal?: string; // supplied by caller for shared caches (Layer 5); undefined for per-client -} - -export interface CachedEntry { - result: unknown; - receivedAt: number; - ttlMs: number; - cacheScope: CacheScope; - scopeExplicit: boolean; // from normalizeCacheMeta — gates cross-principal sharing (Invariant B) -} - -export interface ListCacheStore { - get(key: ListCacheKey): CachedEntry | undefined; - set(key: ListCacheKey, entry: CachedEntry): void; - /** Invalidate entries for a method. See the invalidation matrix below for principal/uri semantics. */ - invalidate(method: ListCacheKey['method'], opts?: { uri?: string; principal?: string }): void; - clear(): void; -} - -export class InMemoryListCacheStore implements ListCacheStore { /* Map-backed, single-tenant */ } -``` - -### Client-local request options (no core pollution) - -`principal`/`bypassCache` are **client-cache concerns and do not belong in core `RequestOptions`** (which is the transport-agnostic, server-and-client per-call bag). They are added in a **client-local** extension instead, keeping the core protocol type pure: - -```typescript -// packages/client/src/client/client.ts -export type ListRequestOptions = RequestOptions & { - principal?: string; // routing key for a shared cache (Layer 5); MUST be the validated auth principal - bypassCache?: boolean; // force refetch, then refresh the cache entry -}; -``` - -The list/read methods widen their `options?: RequestOptions` parameter to `ListRequestOptions` locally. Core `shared/protocol.ts` is **not** modified. - -### ClientOptions - -```typescript -export type ClientOptions = ProtocolOptions & { - // ...existing... - cache?: { - store?: ListCacheStore; // default: new InMemoryListCacheStore() when `cache` present - serveStaleOnError?: boolean; // R-2549-2 "MAY"; default true (resilience) - now?: () => number; // injectable clock for tests; default Date.now (runtime-neutral) - }; -}; -``` - -Absent `cache` ⇒ no caching, current behavior preserved (BC). - -### Read-through in list/read methods - -`listTools`, `listResources`, `listResourceTemplates`, `listPrompts`, `readResource` gain a cache path when caching is enabled: - -1. Build `ListCacheKey` (method + cursor/uri + optional `principal` from `ListRequestOptions`). If `bypassCache` → skip step 2. -2. `entry = store.get(key)`. **Fresh** if `now() < entry.receivedAt + entry.ttlMs` → return cached result. -3. On miss/stale: perform the request. Run `normalizeCacheMeta(parsed, raw)`. **Invariant A:** if `ttlMs === 0`, return the result but **do not** `store.set` (never cache a zero-TTL entry). Otherwise `store.set` with `receivedAt = now()` and the normalized `ttlMs`/`cacheScope`/`scopeExplicit`. -4. On **refetch error** with a prior entry present and `serveStaleOnError` (default true): return the stale result (R-2549-2). Otherwise propagate. - -### Notification invalidation (R-2549-11) - -> **Fix from review:** the existing `_setupListChangedHandlers` only registers when the user passed a `listChanged` config **and** the server advertised the capability — the common cache-user case (no `listChanged` config) would get **no** invalidation, serving stale until TTL expiry and violating R-2549-11. - -When **caching is enabled**, the client registers cache-invalidation notification handlers **unconditionally** — independent of the `listChanged` config and independent of the server capability gate. Because `setNotificationHandler` replaces by method (and would clobber a user's `listChanged` handler), invalidation is wired through an **internal dispatcher**: a single registered handler per notification method that first runs cache invalidation, then delegates to any user-supplied handler. The existing `_setupListChangedHandlers` is refactored to register *through* this dispatcher rather than directly, so cache invalidation and user `onListChanged` callbacks **compose** instead of overwriting each other. - -- `notifications/tools/list_changed` → `store.invalidate('tools/list')` -- `notifications/prompts/list_changed` → `store.invalidate('prompts/list')` -- `notifications/resources/list_changed` → `store.invalidate('resources/list')` **and** `store.invalidate('resources/templates/list')` (conservative; over-invalidates templates on a resource-list change — acceptable, documented) -- `notifications/resources/updated` → `store.invalidate('resources/read', { uri })` for the updated URI - -Invalidation is immediate, regardless of remaining TTL. - -> **`resources/read` invalidation is subscription-dependent (documented).** `notifications/resources/updated` is only sent by the server if the client previously sent `resources/subscribe` for that URI. So notification-driven read-cache invalidation is satisfied **for subscribed URIs only**; for unsubscribed reads, the TTL is the sole staleness bound. Enabling the read cache does **not** auto-subscribe (left to the caller, by design). R-2549-11 status: *satisfied for subscribed resources; TTL-bounded otherwise.* - -### Cursor invalidation (R-2549-14) - -> **Fix from review:** the draft treated *any* `-32602` on *any* cursored fetch as "drop all pages." `-32602` (InvalidParams) is generic and can mean a malformed `params` unrelated to the cursor, masking real bugs in a surprising full-refetch loop. - -Narrowed trigger: the special handling fires **only** when (a) the failing request carried a `cursor` that this cache itself issued from a prior page of the **same list traversal** (a *cache-originated* cursor), **and** (b) the server replied with `ProtocolError` code `-32602`. On both: - -1. `store.invalidate(method)` — drop all cached pages for that list. -2. Re-fetch from page 1 (no cursor). This retry runs with cursor-invalidation **structurally disabled** (it cannot recurse into another page-drop — enforced by a flag scoped to the retry, not by convention), so a second failure propagates. -3. `log`/emit a debug signal when pages are collapsed, so a genuine `-32602` bug is not silently swallowed. - -> **No snapshot isolation across a traversal (documented).** The cache provides **per-page freshness**, not a consistent snapshot of a full paginated list. Each page has its own freshness clock (R-2549-3); a mid-traversal refetch (TTL expiry or cursor invalidation) can yield a page 1 different from the one the caller already consumed. Callers needing a consistent full-list snapshot SHOULD iterate with `bypassCache` or refetch from the beginning, per the SEP. - ---- - -## Layer 4 — Polling Helpers - -**Files:** new `packages/client/src/client/pollList.ts`; export (explicit named) from `packages/client/src/index.ts`. **(PR 2)** - -```typescript -export interface PollListOptions { - onUpdate: (result: unknown) => void; - onError?: (error: unknown) => void; - signal?: AbortSignal; - minIntervalMs?: number; // floor on the poll interval; default 30_000 (see below) - jitter?: number; // fraction, default 0.2 (±20%) - backoff?: { initialMs?: number; maxMs?: number; factor?: number }; // on error - backoffOnUnchanged?: boolean; // grow interval when results are unchanged; default true -} - -// ttlMs is read internally off the response — callers don't extract it. -export function pollList( - client: Client, - method: 'tools/list' | 'prompts/list' | 'resources/list' | 'resources/templates/list', - options: PollListOptions, -): { stop: () => void }; -``` - -> **Fix from review:** the draft's `fetch: () => Promise<{ result, ttlMs }>` leaked `ttlMs` extraction to the caller. `pollList` now takes the `client` + `method` and reads `ttlMs` off the normalized response itself. - -- Base interval seeded from the last response's `ttlMs`, **clamped up** to `minIntervalMs`. -- **`minIntervalMs` default is `30_000`, not `1_000`.** A `ttlMs: 0` server (very common — every pre-2549 server) would otherwise drive a 1-req/sec hammer per list. Polling a `ttlMs: 0` server is **degenerate**; the docs warn against it and the high floor blunts the damage. -- **Jitter** (MUST per R-2549-10): each delay multiplied by `1 ± jitter*random` to avoid thundering herd. -- **Backoff** (MUST per R-2549-10): on error, exponential backoff (`initialMs * factor^n`, capped at `maxMs`) until next success. -- **Backoff on unchanged** (default on): when a poll returns a result equal to the previous one, grow the interval (same capped exponential) so a chatty poller relaxes against a static list; reset on change. Without this, a poller never backs off a never-changing list. -- Opt-in only. Reconciles the SEP's "clients SHOULD NOT use TTL as a polling interval" guidance with R-2549-10: the *default* path is freshness-on-access (Layer 3); `pollList` is a separate utility for callers who explicitly want background refresh, and when used it MUST jitter+backoff. **Docs steer hard toward `pollList`** and explicitly warn against naive `setInterval(ttlMs)` polling — the SDK cannot enforce that a caller won't hand-roll a poll loop, so guidance carries the MUST's intent. - -> **MUST vs SHOULD note (in-code comment).** The SEP MD says polling *SHOULD* jitter+backoff; the canonical `caching.mdx` spec page (and acceptance criterion R-2549-10) escalate this to *MUST*. We implement the stricter MUST. A code comment records the source of the stricter rule so it isn't "relaxed" later by someone reading only the SEP MD. - ---- - -## Layer 5 — Shared (Multi-Tenant) Cache - -**Files:** `packages/client/src/client/listCache.ts` (add `SharedListCacheStore`); export (explicit named) from `packages/client/src/index.ts`. **(PR 2)** - -A `ListCacheStore` implementation for gateways/proxies that front multiple end users through one upstream `Client`. - -### Sharing rule (Invariant B) - -- An entry is shared across principals **only if** `entry.cacheScope === 'public'` **and** `entry.scopeExplicit === true`. A defaulted-public entry (`scopeExplicit === false`) is treated as **private** and is never served to a different principal — fail-safe against pre-2549 / misconfigured servers. -- `cacheScope: 'private'` (or non-explicit) entries are namespaced by `key.principal`. A `get` for such an entry with a non-matching (or absent) principal returns `undefined`. -- Combined with Invariant A (`ttlMs: 0` never stored), this closes the leak path the first draft had: an absent-`cacheScope` user-specific response can never be served cross-user. - -### Principal contract (security-critical) - -- The **principal is supplied by the caller per request** via `ListRequestOptions.principal`. The SDK does not infer identity. -- The principal **MUST be derived from the validated auth principal** (e.g. `ctx.http.authInfo`), an **opaque, stable identifier** — never from request-supplied, attacker-influenceable headers. This is stated as an enforced contract in the API docs, not a passing comment. -- Principal strings are compared **as-is** (no normalization): the store treats distinct representations as distinct namespaces. This is the safe direction (over-isolation, never under-isolation); callers MUST pass a canonical ID. Documented. -- A `private` (or non-explicit) `set` **without** a principal is a **no-op** (the entry cannot be safely isolated, so it is not stored) — documented and tested. - -### Invalidation matrix - -`invalidate(method, opts)` semantics, made explicit (the draft's interface couldn't express these): - -| Event | `opts` | Effect | -|-------|--------|--------| -| `list_changed` for a shared/public list | `{}` | Invalidate the shared public entry for **all** principals **and** every principal's private copy of that method | -| `resources/updated` for a private resource | `{ uri, principal }` | Invalidate only that **principal's** entry for that **uri** | -| `resources/updated`, principal unknown | `{ uri }` | Invalidate that `uri` across **all** principals (conservative) | -| explicit per-principal clear | `{ principal }` | Invalidate that principal's entries for the method | - -Isolation is the security-critical property and gets dedicated tests (below), including the **absent-`cacheScope`** case — not just the explicitly-`private` case — because that is the actual leak path. - ---- - -## R-2549-4 — Notifications Coexist - -No new machinery: `list_changed` notifications already flow through the notification path independent of TTL, and Layer 3's dispatcher composes cache invalidation **with** user `onListChanged` handlers. A test asserts that with caching enabled, a server advertising `listChanged` still delivers notifications **and** the client honors TTL — the two mechanisms layer (notification invalidates immediately; TTL bounds staleness otherwise). - ---- - -## Runtime neutrality - -`packages/client/src/index.ts` is the package root entry and MUST stay runtime-neutral (browser / Cloudflare Workers — no transitive `node:*`). The new modules `listCache.ts` and `pollList.ts` are re-exported from it, so: - -- Both modules MUST be pure JS — no `node:*`, no `crypto` import for principal hashing (principals are opaque caller-supplied strings; the store does not hash). `now` defaults to `Date.now` (neutral) and is injectable. -- Add `listCache.ts` and `pollList.ts` to the package's **`barrelClean` test** so a future accidental Node import is caught. - ---- - -## Testing Strategy - -Tests are TDD-first, one behavior at a time. Layered by package. - -**Core (`packages/core`) — PR 1:** -- `spec.types.test.ts` parity for `CacheableResult` + the 5 result types (compile-time), incl. the `_meta`/looseObject key-parity check. -- Schema unit tests (the **sole** guard for the input contract): absent `ttlMs` → 0; negative → 0; NaN/Infinity → 0; non-integer floored; absent `cacheScope` → `'public'`; explicit values preserved. -- `normalizeCacheMeta`: `scopeExplicit` true when raw payload has `cacheScope`, false when absent; clamp behavior. - -**Server (integration, `test/integration/test/server/mcp.test.ts`) — PR 1:** -- Each of the 4 list endpoints + `resources/read` emits configured `ttlMs`/`cacheScope`. -- `resources/read` callback values override config. -- `resources/read` with `ttlMs > 0` configured + `cacheScope` omitted → emits `'private'` (defense-in-depth). -- Same `cacheScope` across paginated pages (R-2549-8). -- Backward compat: no config → fields present with defaults (0/'public'). -- Low-level `Server` handler can (and must) return the fields directly (ADR-001). - -**Client (`test/integration/test/client/` + unit) — PR 2:** -- Fresh hit served from cache without a wire request; stale triggers refetch (R-2549-2), using injectable `now()`. -- **Invariant A:** `ttlMs: 0` result is never stored and always refetches (R-2549-12). -- Refetch error serves stale when `serveStaleOnError` (default), propagates when off. -- Notification invalidation for each type (R-2549-11), regardless of remaining TTL — **including the no-`listChanged`-config case** (handlers register unconditionally) and the **dispatcher composition** test (cache invalidation + user `onListChanged` both fire, neither clobbers the other). -- `resources/updated` invalidation works for a subscribed URI; documented gap asserted for unsubscribed. -- Cursor `-32602` on a **cache-originated** cursor → drop pages, refetch from page 1; retry is non-recursive (second failure propagates); a `-32602` on a non-cache-originated/non-cursored request does **not** trigger page-drop. -- `pollList`: jitter bounds the interval; backoff grows on consecutive errors then resets; backoff-on-unchanged grows then resets on change; `minIntervalMs` floor honored with `ttlMs: 0` (R-2549-10) — deterministic via injected clock + seeded randomness. -- **`SharedListCacheStore` (R-2549-7):** explicitly-private entry not served cross-principal; **absent-`cacheScope` entry not served cross-principal** (the real leak path); explicit-public entry shared; private `set` without principal is a no-op; the full invalidation matrix. -- Caching disabled by default → behavior identical to today (BC). -- `barrelClean` covers `listCache.ts` / `pollList.ts` (runtime neutrality). - -**E2E requirements manifest (`test/e2e/requirements.ts`):** -Register `caching:*` requirement entries linked to scenario tests so the conformance suite tracks coverage, mirroring existing entries: `caching:ttl:emitted`, `caching:client:freshness`, `caching:invalidate:list-changed`, `caching:invalidate:list-changed:no-config`, `caching:invalidate:resource-updated`, `caching:cursor:invalid`, `caching:scope:isolation`, `caching:scope:isolation:absent-scope`, `caching:poll:jitter-backoff`. Add scenario tests under `test/e2e/scenarios/`. - -## Documentation - -- `docs/migration.md` — human-readable: new fields, McpServer cache (`CacheHints`) config, the **`resources/read` privacy warning**, low-level `Server` breaking change (ADR-001) with the exact two-field snippet, client cache opt-in, the shared-cache **principal contract**, `pollList` (and the anti-pattern warning against `setInterval(ttlMs)`). -- `docs/migration-SKILL.md` — symbol mapping table (`CacheableResult`, `CacheScope`, `CacheHints`, `McpServerOptions`, `ListCacheStore`, `InMemoryListCacheStore`, `SharedListCacheStore`, `ListRequestOptions`, `pollList`, `normalizeCacheMeta`, schema extensions). -- A changeset under `.changeset/` per PR. - -## File Summary - -| Action | File | Layer | PR | -|--------|------|-------|----| -| Modify | `packages/core/src/types/schemas.ts` | 1 | 1 | -| Modify | `packages/core/src/types/types.ts` (+ `normalizeCacheMeta`, `CacheScope`, `CacheableResult`) | 1 | 1 | -| Regenerate | `packages/core/src/types/spec.types.ts` | 1 | 1 | -| Modify | `packages/core/test/spec.types.test.ts` | 1 | 1 | -| Modify | `packages/server/src/server/mcp.ts` | 2 | 1 | -| Modify | `packages/server/src/index.ts` (explicit exports: `CacheHints`, `McpServerOptions`) | 2 | 1 | -| Create | `packages/client/src/client/listCache.ts` | 3, 5 | 2 | -| Modify | `packages/client/src/client/client.ts` (`ListRequestOptions`, read-through, dispatcher) | 3 | 2 | -| Create | `packages/client/src/client/pollList.ts` | 4 | 2 | -| Modify | `packages/client/src/index.ts` (explicit named exports) | 3, 4, 5 | 2 | -| Modify | `test/integration/test/server/mcp.test.ts` | 2 | 1 | -| Create/Modify | `test/integration/test/client/*` | 3, 4, 5 | 2 | -| Modify | `test/e2e/requirements.ts` + `test/e2e/scenarios/*` | all | 1 & 2 | -| Modify | `docs/migration.md`, `docs/migration-SKILL.md`, `.changeset/*` | all | 1 & 2 | - -> **Note:** `packages/core/src/shared/protocol.ts` is **no longer modified.** `principal`/`bypassCache` moved to the client-local `ListRequestOptions` (Layer 3) to keep core protocol pure. - -## Review Findings → Resolution - -Traceability for the backend + software architecture review (rev 1 → rev 2): - -| Finding | Resolution | -|---------|------------| -| `cacheScope` default `'public'` leaks cross-tenant (CRITICAL ×2) | Invariants A & B; `scopeExplicit` on `CachedEntry`; `SharedListCacheStore` shares only explicit-public; `resources/read` defense-in-depth `'private'` | -| Notification invalidation gated by `listChanged` config/capability (CRITICAL) | Unconditional registration when caching on; internal dispatcher composes with user handlers; `no-config` test | -| `resources/updated` only fires when subscribed | Documented as subscription-dependent; R-2549-11 status qualified; asserted by test | -| Low-level `Server` breaking change rationale | ADR-001 (keep break; reject `z.input` softening; protocol does not back-fill outbound; McpServer is internal consumer) | -| `principal`/`bypassCache` pollute core `RequestOptions` | Moved to client-local `ListRequestOptions`; core untouched | -| `.transform()` brittle / lossy | Dropped; `.default()` + `normalizeCacheMeta` helper; `scopeExplicit` preserved | -| `-32602` cursor narrowing too broad | Scoped to cache-originated cursors; non-recursive single retry; logs on collapse; no-snapshot-isolation documented | -| Shared-cache bypass paths (F2) | Principal-derivation contract (auth principal only, opaque, no normalization); private-set-without-principal no-op; invalidation matrix; absent-scope test | -| Export plan `export *` framing | Split: two core types via sanctioned `export *`; all package-barrel symbols explicit named | -| `CacheableResult` `@internal` in spec | Deliberately public; divergence noted in export comment | -| Parity covers `z.output` only | Stated; input contract guarded by unit tests | -| `_meta`/looseObject key-parity interplay | Explicit verification step in spec-test bookkeeping | -| Type vs spread manual-sync seam | Called out (mirrors `PaginatedResult`) | -| `ListCacheConfig` prefix collision | Renamed server-side to `CacheHints` | -| `pollList` leaks `ttlMs` extraction | Signature takes `client` + `method`; reads `ttlMs` internally | -| `pollList` `minIntervalMs` 1s hammer / no unchanged-backoff | Default `30_000`; degenerate `ttlMs:0` warned; `backoffOnUnchanged` default on | -| Runtime neutrality of new modules | Pure-JS requirement + `barrelClean` coverage | -| API surface too large | Split into PR 1 (wire+server) and PR 2 (client cache) | -| `ttlMs:0` invariant under-stated | Promoted to Invariant A with dedicated test | -| R-2549-8 for low-level servers | Documented as author responsibility (no runtime guard) | - -## Open Risks - -1. **Exact Zod v4 `.default()` output-type form.** `z.number().default(0)` spread into a `looseObject` result schema must yield a *required* `number` in `z.output` and pass `AssertExactKeys`. De-risked by a compile-only spike done before Layer 3 (see Layer 1). Fallback: clamp/normalize entirely outside the schema (already the plan via `normalizeCacheMeta`). -2. **Dispatcher refactor of `_setupListChangedHandlers`.** Routing cache invalidation and user `onListChanged` through one composing dispatcher touches existing notification wiring. Covered by the composition test; verify no regression for users who configured `listChanged` without caching. -3. **Manual sync between exported `CacheableResult` type and the `cacheableResultFields` spread** (no shared schema). Low risk, mirrors `PaginatedResult`; parity test catches divergence at the result-type level. diff --git a/docs/superpowers/specs/2026-06-23-sdk-shared-package-design.md b/docs/superpowers/specs/2026-06-23-sdk-shared-package-design.md deleted file mode 100644 index 4f9f4157c0..0000000000 --- a/docs/superpowers/specs/2026-06-23-sdk-shared-package-design.md +++ /dev/null @@ -1,135 +0,0 @@ -# Design: `@modelcontextprotocol/sdk-shared` — canonical Zod schemas package - -- **Date:** 2026-06-23 -- **Status:** Approved (design); implementation plan pending -- **Owner:** Konstantin Konstantinov - -## Problem - -The v1→v2 migration of runtime schema validation is non-mechanical and lossy. - -In v1, consumers validated values with the exported Zod schema constants: - -```ts -import { CallToolResultSchema, ListToolsResultSchema } from '@modelcontextprotocol/sdk/types.js'; - -const parsed = ListToolsResultSchema.parse(res.body.result); // throws on invalid, returns value -const r = CallToolResultSchema.safeParse(value); // { success, data, error } -``` - -In v2 these schemas are reached via `specTypeSchemas.X` and **typed** as `StandardSchemaV1` (to keep Zod out of the public API), even though **at runtime they are still the underlying Zod schemas**. Because the public type is Standard Schema, `.parse()`/`.safeParse()` are not visible to the type checker, so the current codemod: - -- rewrites `CallToolResultSchema` → `specTypeSchemas.CallToolResult`, -- converts `.safeParse(x)` → `specTypeSchemas.X['~standard'].validate(x)` and remaps `.success`/`.data`/`.error` (which also changes the thrown error type), and -- has **no** one-line equivalent for `.parse()` (it throws; `validate()` does not), so those sites get a manual-migration diagnostic and don't compile until hand-edited. - -Validated against `firebase/firebase-tools`, this produced 4 post-codemod typecheck errors (all `.parse()`), plus project-type-resolution warnings on type-only files. - -A prior attempt (PR #2277) surfaces `parse()`/`safeParse()` on each `specTypeSchemas.X` entry as **type-only** methods and migrates by reference rename. That works but (a) pollutes the deliberately library-agnostic Standard Schema type with Zod-specific methods, and (b) only covers `parse`/`safeParse`, not other Zod methods (`.extend()`, `.merge()`, `.shape`, …). - -## Goals - -- Make schema-validation migration a **mechanical, behavior-preserving import-path swap**: `.parse()`/`.safeParse()` and every other Zod method keep working unchanged. -- Keep the `server`/`client` main API surface **Zod-free**; Zod coupling is opt-in and explicit. -- Establish a **canonical home for shared spec primitives** (schemas + types now, room for more later). -- Keep `specTypeSchemas`/`isSpecType` (the Standard Schema, library-agnostic view) intact and recommended for library-agnostic validation. - -## Non-goals - -- Changing the Standard Schema typing of `specTypeSchemas` (we are **not** adding `parse`/`safeParse` to it — this supersedes PR #2277's approach). -- Moving `Protocol`, transports, or validators. They stay in `core` and follow existing migration rules. -- Moving `specTypeSchemas`/`isSpecType` out of `core/public` (possible later; out of scope now). - -> **Update during implementation (Option C, user-approved):** the protocol **enums** (`enums.ts` → -> `ProtocolErrorCode`), **error classes** (`errors.ts` → `ProtocolError`, …), and **type guards** -> (`guards.ts`) were *also* moved into `sdk-shared`, reversing the original "they stay in core" -> non-goal. Rationale: v1's `sdk/types.js` was a kitchen-sink exporting all of these alongside the -> spec types/schemas, so the codemod's `types.js → sdk-shared` routing is only correct if sdk-shared -> carries that whole surface. Their dependency closure (schemas/types/enums) is already in sdk-shared, -> so the move is clean and introduces no cycle. **Exception:** `SdkError`/`SdkErrorCode`/`SdkHttpError` -> (the SDK-side error split, in `core/errors/sdkErrors.ts`) deliberately stay in `core` → `server`/`client`. - -## Decisions (locked) - -| Decision | Choice | -| --- | --- | -| Package name | `@modelcontextprotocol/sdk-shared` | -| Scope of move | Spec **types + Zod `*Schema` constants** | -| Positioning | Zod schemas are **first-class** (no codemod nudge toward `specTypeSchemas`) | -| server/client bundling | Depend on `sdk-shared` as a **regular dependency**, marked **external** (not bundled) | -| server/client re-exports | Re-export **types** from `sdk-shared`; do **not** re-export the raw Zod `*Schema` constants | -| Consumer dependency | Regular `dependency` (not peer); codemod adds it | -| core churn control | `core`'s internal barrel **re-exports** schemas/types from `sdk-shared` | - -## Architecture - -### Package - -New public package `packages/sdk-shared/` (`@modelcontextprotocol/sdk-shared`): - -- Owns the canonical MCP spec data model: the Zod `*Schema` constants and their derived TS types (`Tool`, `CallToolResult`, …), extracted from `packages/core/src/types/types.ts`. -- Depends only on `zod` (catalog: `runtimeShared`). **Runtime-neutral** — no Node builtins — so browser/Cloudflare Workers bundlers can consume it (covered by a `barrelClean` test, per CLAUDE.md). -- Uses explicit named exports. - -### Dependency graph (new edges in **bold**) - -``` -zod - └── @modelcontextprotocol/sdk-shared (NEW — types + Zod *Schema constants; zod-only) - ├── @modelcontextprotocol/core (private; imports schemas/types from sdk-shared, re-exports them from its barrel) - ├── **@modelcontextprotocol/server** ─┐ regular dependency, - └── **@modelcontextprotocol/client** ─┘ marked EXTERNAL in tsdown (not bundled) -``` - -`server`/`client` today inline `core` (and thus the schemas). After this change they treat `@modelcontextprotocol/sdk-shared` as an external dependency, so there is a single runtime instance and their bundles shrink. (Instance identity is not a correctness concern — validation is structural — so "single instance" is about source-of-truth and bundle size, not behavior.) - -### What moves vs. stays - -- **Moves to `sdk-shared`:** the spec Zod `*Schema` constants and their inferred TS types. `types.ts` is **split** along this line; the exact boundary (pure spec schemas + inferred types move; protocol constants such as `LATEST_PROTOCOL_VERSION` and method-name constants stay in `core` for now) is finalized during implementation. `core`'s barrel keeps re-exporting the moved symbols so the ~hundreds of internal `core` imports don't all change. -- **Stays in `core`:** `Protocol`, transports, validators, error classes/enums, protocol constants, and `specTypeSchemas`/`isSpecType` (rebuilt from `sdk-shared`'s schemas, exported via `core/public` as today). - -### Public API surface after the change - -| Symbol kind | Canonical home | Also re-exported by | Typed as | -| --- | --- | --- | --- | -| Spec **types** (`Tool`, `CallToolResult`, …) | `sdk-shared` | `core/public`, `server`, `client` | TS types (Zod-free) | -| Zod **`*Schema` constants** | `sdk-shared` **only** | — (intentionally not on server/client) | real Zod schemas | -| `specTypeSchemas` / `isSpecType` | `core/public` | `server`, `client` | `StandardSchemaV1` (Zod-free) | - -Guidance: use `specTypeSchemas` for library-agnostic Standard Schema validation; import the Zod `*Schema` from `@modelcontextprotocol/sdk-shared` when you want Zod ergonomics (`.parse`, `.safeParse`, `.extend`, …) or are migrating v1 code. - -## Codemod changes - -Today: the `imports` transform sends `sdk/types.js` → `RESOLVE_BY_CONTEXT`; the `specSchemaAccess` transform rewrites the schema reference, converts `.safeParse()`, and emits a manual-migration diagnostic for `.parse()`. - -After: - -1. **`@modelcontextprotocol/sdk/types.js`** (and the extensionless `/types`, already handled) **→ `@modelcontextprotocol/sdk-shared`**: a fixed, context-free path swap covering both types and `*Schema` constants. Symbol names unchanged; existing `renamedSymbols` (e.g. `ResourceTemplate`→`ResourceTemplateType`) still apply. -2. **Retire `specSchemaAccess`'s schema rewriting.** Because `sdk-shared` exports real Zod schemas, `.parse()`/`.safeParse()`/`.extend()`/`.shape`/… all keep working untouched — no reference rename, no `.safeParse` result remap, no `.parse()` manual-migration diagnostic. The independent `schemaParamRemoval` transform (strips schema args from `request()`/`callTool()`) is unaffected and stays. -3. **`updatePackageJson` adds `@modelcontextprotocol/sdk-shared`** to the consumer whenever a `types.js` import is routed there. - -Expected effect on `firebase/firebase-tools`: the 4 `.parse()` errors disappear (schemas validate via Zod as before) and the project-type warnings on type-only files disappear (fixed target, no context resolution) → **zero codemod-introduced typecheck errors**, far fewer diagnostics. - -## Testing strategy - -- **`sdk-shared` package:** unit tests asserting expected exports exist; `barrelClean` test (no Node builtins); runtime-neutral. -- **`codemod`:** update `importPaths` tests (`types.js`/`/types` → `sdk-shared`); remove/trim `specSchemaAccess` tests; add coverage for the dependency addition and "schema usage passes through untouched." -- **`core`/`server`/`client`:** existing suites + typecheck stay green after the `types.ts` split (the main risk). -- **Batch test:** add `sdk-shared` to `LOCAL_PACKAGE_DIRS`; add an `overrides` entry so the transitive `server`→`sdk-shared` edge resolves to the local tarball; re-run `firebase/firebase-tools` and confirm 0 introduced typecheck errors. - -## Docs & rollout - -- Rewrite the spec-schema validation section in `docs/migration.md` and `docs/migration-SKILL.md`: schemas now import from `@modelcontextprotocol/sdk-shared`, `.parse`/`.safeParse` keep working; `specTypeSchemas` remains for library-agnostic validation. Document the new package. -- Add a changeset covering the new package and the codemod change. -- **PR #2277 coordination:** this **supersedes** #2277's `specTypeSchemas` type-only `parse`/`safeParse` approach. Its other improvements are independent and worth keeping: client/server inference (#2) and the `tasks/*` handler-map fix (#3). The extensionless-import fix (#4) is already implemented on this branch. - -## Risks & mitigations - -- **Splitting `types.ts`** is wide-reaching. Mitigation: keep `core`'s barrel re-exporting the moved symbols; land the move as its own step with full `core` typecheck/tests green before touching the codemod. -- **Transitive local-tarball resolution** in the batch test (`server`→`sdk-shared`). Mitigation: `overrides` entry pointing `sdk-shared` at the local tarball (or publish an alpha). -- **New publish/version target.** Mitigation: version `sdk-shared` in lockstep with the other v2 packages via changesets. - -## Open questions (non-blocking) - -- Final split boundary inside `types.ts` (which non-schema symbols, if any, also belong in `sdk-shared`). -- Whether `specTypeSchemas`/`isSpecType` should eventually move to `sdk-shared` too (deferred). From c2e70af1f2d5272f34b91cffc0d7668fe1701b8f Mon Sep 17 00:00:00 2001 From: Konstantin Konstantinov Date: Thu, 25 Jun 2026 09:01:29 +0300 Subject: [PATCH 08/21] fix(codemod): split aliased imports per-symbol, drop undefined schema arg, preserve import comments - importPaths: aliased named imports now route through the per-symbol splitter (addOrMergeImport carries aliases), so a mixed type+schema import no longer collapses into one v2 package and mis-routes schema constants - schemaParamRemoval: drop a literal `undefined` result-schema argument from request()/callTool() when an options arg follows - importPaths: preserve the first SDK import's leading header/JSDoc comment across the rewrite - batch-test: install pnpm clones standalone (--ignore-workspace --no-frozen-lockfile, CI=true) so repos nested in this workspace actually install --- packages/codemod/src/bin/batchTest.ts | 26 ++++++- .../v1-to-v2/transforms/importPaths.ts | 75 ++++++++++--------- .../v1-to-v2/transforms/schemaParamRemoval.ts | 13 ++++ packages/codemod/src/utils/importUtils.ts | 33 ++++++-- .../v1-to-v2/transforms/importPaths.test.ts | 50 ++++++++++++- .../transforms/schemaParamRemoval.test.ts | 25 +++++++ 6 files changed, 173 insertions(+), 49 deletions(-) diff --git a/packages/codemod/src/bin/batchTest.ts b/packages/codemod/src/bin/batchTest.ts index eefe0f2df5..913db845a8 100644 --- a/packages/codemod/src/bin/batchTest.ts +++ b/packages/codemod/src/bin/batchTest.ts @@ -111,6 +111,22 @@ function detectPm(repoRoot: string): string { return 'npm'; } +function installCommand(pm: 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. + return 'pnpm install --ignore-scripts --ignore-workspace --no-frozen-lockfile'; +} + function detectCheckCmd(pkgDir: string, checkType: string): string | null { const pkgJsonPath = path.join(pkgDir, 'package.json'); if (!existsSync(pkgJsonPath)) return null; @@ -133,7 +149,11 @@ function shell(cmd: string, cwd?: string): { exitCode: number; stdout: string; s cwd, stdio: ['pipe', 'pipe', 'pipe'], maxBuffer: 10 * 1024 * 1024, - timeout: 5 * 60 * 1000 + timeout: 5 * 60 * 1000, + // Commands are spawned without a TTY (piped stdio). Set CI so package managers run fully + // non-interactively — without it, pnpm aborts rebuilding a clone's modules dir with + // ERR_PNPM_ABORTED_REMOVE_MODULES_DIR_NO_TTY when --ignore-workspace changes its link mode. + env: { ...process.env, CI: 'true' } }).toString(); return { exitCode: 0, stdout, stderr: '' }; } catch (error: unknown) { @@ -319,7 +339,7 @@ function main(): void { // Step 3: Install console.log(' Installing dependencies...'); - const installResult = shell(`${pm} install --ignore-scripts`, clonePath); + const installResult = shell(installCommand(pm), clonePath); if (installResult.exitCode !== 0) { console.log(` ERROR: install failed, skipping\n ${installResult.stderr.split('\n')[0]}`); continue; @@ -367,7 +387,7 @@ function main(): void { console.log(` Rewrote ${rewrites} deps to local tarballs`); } console.log(' Re-installing dependencies...'); - shell(`${pm} install --ignore-scripts`, clonePath); + shell(installCommand(pm), clonePath); // Step 7: Post-codemod checks console.log(' Running post-codemod checks...'); diff --git a/packages/codemod/src/migrations/v1-to-v2/transforms/importPaths.ts b/packages/codemod/src/migrations/v1-to-v2/transforms/importPaths.ts index fce2edf760..7ab0b724b7 100644 --- a/packages/codemod/src/migrations/v1-to-v2/transforms/importPaths.ts +++ b/packages/codemod/src/migrations/v1-to-v2/transforms/importPaths.ts @@ -4,6 +4,7 @@ import { SyntaxKind } from 'ts-morph'; import type { Diagnostic, Transform, TransformContext, TransformResult } from '../../../types.js'; import { renameAllReferences } from '../../../utils/astUtils.js'; import { actionRequired, info, v2Gap, warning } from '../../../utils/diagnostics.js'; +import type { NamedImportSpec } from '../../../utils/importUtils.js'; import { addOrMergeImport, getSdkExports, getSdkImports, isTypeOnlyImport } from '../../../utils/importUtils.js'; import { resolveTypesPackage } from '../../../utils/projectAnalyzer.js'; import type { ImportMapping } from '../mappings/importMap.js'; @@ -75,17 +76,25 @@ export const importPathsTransform: Transform = { const insertIndex = sourceFile.getImportDeclarations().indexOf(sdkImports[0]!); + // A leading file-header / JSDoc comment attaches to the first SDK import as leading trivia. When + // that import is removed and re-emitted (the per-symbol split/merge path calls imp.remove()), + // ts-morph drops the comment with it. Capture it now and restore it after emitting if it was lost. + const leadingCommentText = sdkImports[0]! + .getLeadingCommentRanges() + .map(r => r.getText()) + .join('\n'); + interface PendingImport { - names: string[]; + specs: NamedImportSpec[]; isTypeOnly: boolean; } const pendingImports = new Map(); - function addPending(target: string, names: string[], isTypeOnly: boolean): void { + function addPending(target: string, specs: NamedImportSpec[], isTypeOnly: boolean): void { if (!pendingImports.has(target)) { pendingImports.set(target, []); } - pendingImports.get(target)!.push({ names, isTypeOnly }); + pendingImports.get(target)!.push({ specs, isTypeOnly }); } for (const imp of sdkImports) { @@ -141,26 +150,11 @@ export const importPathsTransform: Transform = { } } - const hasAlias = namedImports.some(n => n.getAliasNode() !== undefined); - if (defaultImport || namespaceImport || hasAlias) { - let effectiveTarget = targetPackage; - if ((mapping.symbolTargetOverrides || mapping.schemaSymbolTarget) && !namespaceImport && !defaultImport) { - const overrides = namedImports.map(n => symbolTargetOverride(n.getName(), mapping)); - const uniqueOverrides = new Set(overrides.filter((t): t is string => t !== undefined)); - const allOverridden = namedImports.length > 0 && overrides.every(t => t !== undefined); - if (allOverridden && uniqueOverrides.size === 1) { - effectiveTarget = [...uniqueOverrides][0]!; - } else if (uniqueOverrides.size > 0) { - diagnostics.push( - actionRequired( - filePath, - imp, - `Aliased import from ${specifier} mixes symbols that belong to different v2 packages. ` + - `Split the import manually so each symbol targets the correct package.` - ) - ); - } - } + // Default and namespace imports cannot be split per-symbol — the whole binding moves to one + // package. Named imports (aliased or not) fall through to the per-symbol splitter below, so a + // single aliased specifier no longer forces unrelated symbols into the wrong package. + if (defaultImport || namespaceImport) { + const effectiveTarget = targetPackage; // A namespace import (`import * as ns from '…/types.js'`) cannot be split per-symbol, so // any `ns.Schema` accesses would silently resolve against the wrong package. Flag them. if (namespaceImport && mapping.schemaSymbolTarget) { @@ -220,11 +214,12 @@ export const importPathsTransform: Transform = { for (const n of namedImports) { const name = n.getName(); + const alias = n.getAliasNode()?.getText(); const resolvedName = mapping.renamedSymbols?.[name] ?? name; const specifierTypeOnly = typeOnly || n.isTypeOnly(); const symbolTarget = symbolTargetOverride(name, mapping) ?? targetPackage; usedPackages.add(symbolTarget); - addPending(symbolTarget, [resolvedName], specifierTypeOnly); + addPending(symbolTarget, [alias ? { name: resolvedName, alias } : resolvedName], specifierTypeOnly); } imp.remove(); changesCount++; @@ -236,28 +231,34 @@ export const importPathsTransform: Transform = { } } + const specLocal = (spec: NamedImportSpec): string => (typeof spec === 'string' ? spec : (spec.alias ?? spec.name)); for (const [target, groups] of pendingImports) { - const typeOnlyNames = new Set(); - const valueNames = new Set(); + // Dedupe by local binding name (alias when present), keeping the spec so aliases survive. + const typeOnlySpecs = new Map(); + const valueSpecs = new Map(); for (const group of groups) { - for (const name of group.names) { - if (group.isTypeOnly) { - typeOnlyNames.add(name); - } else { - valueNames.add(name); - } + for (const spec of group.specs) { + (group.isTypeOnly ? typeOnlySpecs : valueSpecs).set(specLocal(spec), spec); } } - if (valueNames.size > 0) { - addOrMergeImport(sourceFile, target, [...valueNames], false, insertIndex); + if (valueSpecs.size > 0) { + addOrMergeImport(sourceFile, target, [...valueSpecs.values()], false, insertIndex); } - if (typeOnlyNames.size > 0) { - const typeInsertIndex = valueNames.size > 0 ? insertIndex + 1 : insertIndex; - addOrMergeImport(sourceFile, target, [...typeOnlyNames], true, typeInsertIndex); + if (typeOnlySpecs.size > 0) { + const typeInsertIndex = valueSpecs.size > 0 ? insertIndex + 1 : insertIndex; + addOrMergeImport(sourceFile, target, [...typeOnlySpecs.values()], true, typeInsertIndex); } } + // Restore the captured leading comment if the rewrite dropped it (guard against duplication when + // the first import was rewritten in place and kept its comment). + if (leadingCommentText && !sourceFile.getFullText().includes(leadingCommentText)) { + const imports = sourceFile.getImportDeclarations(); + const anchor = imports[Math.min(insertIndex, imports.length - 1)]; + sourceFile.insertText(anchor ? anchor.getStart() : 0, `${leadingCommentText}\n`); + } + return { changesCount, diagnostics, usedPackages }; } }; diff --git a/packages/codemod/src/migrations/v1-to-v2/transforms/schemaParamRemoval.ts b/packages/codemod/src/migrations/v1-to-v2/transforms/schemaParamRemoval.ts index eea8e33fd8..2e9737d383 100644 --- a/packages/codemod/src/migrations/v1-to-v2/transforms/schemaParamRemoval.ts +++ b/packages/codemod/src/migrations/v1-to-v2/transforms/schemaParamRemoval.ts @@ -27,6 +27,19 @@ export const schemaParamRemovalTransform: Transform = { const secondArg = args[1]!; if (!Node.isIdentifier(secondArg)) continue; + // `request(req, undefined, options)` / `callTool(params, undefined, options)`: v1 passed an + // explicit `undefined` result schema before the trailing options argument. v2 removed the + // schema parameter for spec methods, so the literal `undefined` leaves the call with one + // argument too many (TS2554). Drop it only when a third argument follows — a 2-arg + // `callTool(params, undefined)` already type-checks, since `undefined` is a valid options arg. + if (secondArg.getText() === 'undefined') { + if (args.length >= 3) { + call.removeArgument(1); + changesCount++; + } + continue; + } + const schemaName = secondArg.getText(); const originalName = resolveOriginalImportName(sourceFile, schemaName) ?? schemaName; if (!originalName.endsWith('Schema')) continue; diff --git a/packages/codemod/src/utils/importUtils.ts b/packages/codemod/src/utils/importUtils.ts index a1981cb14f..5ad92cd25b 100644 --- a/packages/codemod/src/utils/importUtils.ts +++ b/packages/codemod/src/utils/importUtils.ts @@ -33,31 +33,52 @@ export function isTypeOnlyImport(imp: ImportDeclaration): boolean { return imp.isTypeOnly(); } +/** A named import to emit: either a bare name, or a `{ name, alias }` pair preserving an `as` alias. */ +export type NamedImportSpec = string | { name: string; alias?: string }; + +function toSpec(n: NamedImportSpec): { name: string; alias?: string } { + return typeof n === 'string' ? { name: n } : n; +} + +/** Local binding a spec introduces — the alias when present, otherwise the imported name. */ +function specLocalName(s: { name: string; alias?: string }): string { + return s.alias ?? s.name; +} + export function addOrMergeImport( sourceFile: SourceFile, moduleSpecifier: string, - namedImports: string[], + namedImports: NamedImportSpec[], isTypeOnly: boolean, insertIndex: number ): void { if (namedImports.length === 0) return; + const specs = namedImports.map(n => toSpec(n)); + const existing = sourceFile.getImportDeclarations().find(imp => { if (imp.getNamespaceImport()) return false; return imp.getModuleSpecifierValue() === moduleSpecifier && imp.isTypeOnly() === isTypeOnly; }); if (existing) { - const existingNames = new Set(existing.getNamedImports().map(n => n.getName())); - const newNames = namedImports.filter(n => !existingNames.has(n)); - if (newNames.length > 0) { - existing.addNamedImports(newNames); + const existingLocals = new Set(existing.getNamedImports().map(n => n.getAliasNode()?.getText() ?? n.getName())); + const newSpecs = specs.filter(s => !existingLocals.has(specLocalName(s))); + if (newSpecs.length > 0) { + existing.addNamedImports(newSpecs.map(s => (s.alias ? { name: s.name, alias: s.alias } : { name: s.name }))); } } else { + const seen = new Set(); + const deduped = specs.filter(s => { + const local = specLocalName(s); + if (seen.has(local)) return false; + seen.add(local); + return true; + }); const clampedIndex = Math.min(insertIndex, sourceFile.getImportDeclarations().length); sourceFile.insertImportDeclaration(clampedIndex, { moduleSpecifier, - namedImports: [...new Set(namedImports)], + namedImports: deduped.map(s => (s.alias ? { name: s.name, alias: s.alias } : { name: s.name })), isTypeOnly }); } diff --git a/packages/codemod/test/v1-to-v2/transforms/importPaths.test.ts b/packages/codemod/test/v1-to-v2/transforms/importPaths.test.ts index e58a699f89..326579fe79 100644 --- a/packages/codemod/test/v1-to-v2/transforms/importPaths.test.ts +++ b/packages/codemod/test/v1-to-v2/transforms/importPaths.test.ts @@ -131,6 +131,45 @@ describe('import-paths transform', () => { expect(result).not.toContain('@modelcontextprotocol/sdk/types'); }); + it('splits an aliased types.js import: schema constant to sdk-shared, aliased type to server', () => { + // The presence of an alias (`Tool as SDKTool`) must not force the whole import into one package; + // each symbol still routes to its correct v2 target, with the alias preserved. + const input = [ + `import { CreateMessageRequestSchema, ClientCapabilities, Tool as SDKTool } from '@modelcontextprotocol/sdk/types.js';`, + '' + ].join('\n'); + const result = applyTransform(input, { projectType: 'server' }); + expect(result).toMatch(/import\s*\{[^}]*\bCreateMessageRequestSchema\b[^}]*\}\s*from\s*["']@modelcontextprotocol\/sdk-shared["']/); + expect(result).toMatch(/import\s*\{[^}]*\bClientCapabilities\b[^}]*\}\s*from\s*["']@modelcontextprotocol\/server["']/); + expect(result).toContain('Tool as SDKTool'); + // the schema constant must NOT end up imported from @modelcontextprotocol/server + expect(result).not.toMatch(/import\s*\{[^}]*CreateMessageRequestSchema[^}]*\}\s*from\s*["']@modelcontextprotocol\/server["']/); + expect(result).not.toContain('@modelcontextprotocol/sdk/types'); + }); + + it('does not emit a "mixes symbols" diagnostic for an aliased mixed import (it splits instead)', () => { + const input = `import { CreateMessageRequestSchema, Tool as SDKTool } from '@modelcontextprotocol/sdk/types.js';\n`; + const project = new Project({ useInMemoryFileSystem: true }); + const sourceFile = project.createSourceFile('test.ts', input); + const result = importPathsTransform.apply(sourceFile, { projectType: 'server' }); + expect(result.diagnostics.some(d => d.message.includes('mixes symbols'))).toBe(false); + }); + + it('preserves a leading file-header comment when rewriting the first SDK import', () => { + const input = [ + `/**`, + ` * Web-standard transport for MCP.`, + ` */`, + `import { Transport } from '@modelcontextprotocol/sdk/shared/transport.js';`, + `import { JSONRPCMessage } from '@modelcontextprotocol/sdk/types.js';`, + '' + ].join('\n'); + const result = applyTransform(input, { projectType: 'server' }); + expect(result).toContain('Web-standard transport for MCP.'); + expect(result).toContain('@modelcontextprotocol/server'); + expect(result).not.toContain('@modelcontextprotocol/sdk/'); + }); + it('does not rewrite schema .parse() usages (migrates as an import-path swap)', () => { const input = [ `import { CallToolResultSchema } from '@modelcontextprotocol/sdk/types.js';`, @@ -562,7 +601,7 @@ describe('import-paths transform', () => { expect(result.diagnostics[0]!.message).toContain('RequestHandlerExtra'); }); - it('emits warning for aliased import mixing symbols from different v2 packages', () => { + it('splits an aliased import mixing symbols from different v2 packages (no longer bails)', () => { const input = [ `import { StreamableHTTPServerTransport as T, EventStore } from '@modelcontextprotocol/sdk/server/streamableHttp.js';`, '' @@ -570,8 +609,13 @@ describe('import-paths transform', () => { const project = new Project({ useInMemoryFileSystem: true }); const sourceFile = project.createSourceFile('test.ts', input); const result = importPathsTransform.apply(sourceFile, { projectType: 'server' }); - expect(result.diagnostics.length).toBeGreaterThan(0); - expect(result.diagnostics.some(d => d.message.includes('mixes symbols') && d.message.includes('Split'))).toBe(true); + const output = sourceFile.getFullText(); + // transport (aliased + renamed) → /node; companion type → /server + expect(output).toContain('NodeStreamableHTTPServerTransport as T'); + expect(output).toContain('@modelcontextprotocol/node'); + expect(output).toMatch(/import\s*\{[^}]*\bEventStore\b[^}]*\}\s*from\s*["']@modelcontextprotocol\/server["']/); + expect(output).not.toContain('@modelcontextprotocol/sdk'); + expect(result.diagnostics.some(d => d.message.includes('mixes symbols'))).toBe(false); }); it('emits warning for re-export mixing symbols from different v2 packages', () => { diff --git a/packages/codemod/test/v1-to-v2/transforms/schemaParamRemoval.test.ts b/packages/codemod/test/v1-to-v2/transforms/schemaParamRemoval.test.ts index f1a2413982..3547589e0c 100644 --- a/packages/codemod/test/v1-to-v2/transforms/schemaParamRemoval.test.ts +++ b/packages/codemod/test/v1-to-v2/transforms/schemaParamRemoval.test.ts @@ -117,4 +117,29 @@ describe('schema-param-removal transform', () => { const result = applyTransform(input); expect(result).not.toMatch(/import.*CallToolResultSchema/); }); + + it('removes a literal undefined schema slot from callTool when an options argument follows', () => { + const input = [ + `const result = await client.callTool({ name: 'add', arguments: { a: 1 } }, undefined, { onprogress: cb });`, + '' + ].join('\n'); + const result = applyTransform(input); + expect(result).toContain("client.callTool({ name: 'add', arguments: { a: 1 } }, { onprogress: cb })"); + expect(result).not.toContain('undefined'); + }); + + it('removes a literal undefined schema slot from request when an options argument follows', () => { + const input = [`const result = await client.request({ method: 'tools/call', params: {} }, undefined, { timeout: 5000 });`, ''].join( + '\n' + ); + const result = applyTransform(input); + expect(result).toContain("client.request({ method: 'tools/call', params: {} }, { timeout: 5000 })"); + expect(result).not.toContain('undefined'); + }); + + it('leaves a 2-arg callTool(params, undefined) unchanged (already valid as options in v2)', () => { + const input = [`await client.callTool({ name: 'add' }, undefined);`, ''].join('\n'); + const result = applyTransform(input); + expect(result).toContain('undefined'); + }); }); From 4a15488401e868b383522f2b81189fa37daaf2ee Mon Sep 17 00:00:00 2001 From: Konstantin Konstantinov Date: Thu, 25 Jun 2026 10:43:08 +0300 Subject: [PATCH 09/21] fixes --- docs/migration-SKILL.md | 2 +- docs/migration.md | 2 +- .../v1-to-v2/mappings/authSchemaNames.ts | 21 +++++++ .../migrations/v1-to-v2/mappings/importMap.ts | 20 ++++--- .../v1-to-v2/transforms/importPaths.ts | 43 ++++++++++----- .../v1-to-v2/transforms/schemaParamRemoval.ts | 9 ++- .../test/v1-to-v2/authSchemaNames.test.ts | 27 +++++++++ .../v1-to-v2/transforms/importPaths.test.ts | 34 ++++++++++++ .../transforms/schemaParamRemoval.test.ts | 21 +++++-- packages/core/src/exports/public/index.ts | 1 + packages/core/src/types/specTypeSchema.ts | 2 + .../core/test/types/specTypeSchema.test.ts | 7 ++- packages/sdk-shared/src/index.ts | 40 +++++++++++--- .../sdk-shared/test/sdkSharedSchemas.test.ts | 55 +++++++++++++------ packages/sdk-shared/tsconfig.json | 3 +- packages/sdk-shared/tsdown.config.ts | 20 ++++--- 16 files changed, 239 insertions(+), 68 deletions(-) create mode 100644 packages/codemod/src/migrations/v1-to-v2/mappings/authSchemaNames.ts create mode 100644 packages/codemod/test/v1-to-v2/authSchemaNames.test.ts diff --git a/docs/migration-SKILL.md b/docs/migration-SKILL.md index d61fb1a344..d13e4c69c1 100644 --- a/docs/migration-SKILL.md +++ b/docs/migration-SKILL.md @@ -65,7 +65,7 @@ Replace all `@modelcontextprotocol/sdk/...` imports using this table. | `@modelcontextprotocol/sdk/shared/protocol.js` | `@modelcontextprotocol/client` or `@modelcontextprotocol/server` | | `@modelcontextprotocol/sdk/shared/transport.js` | `@modelcontextprotocol/client` or `@modelcontextprotocol/server` | | `@modelcontextprotocol/sdk/shared/uriTemplate.js` | `@modelcontextprotocol/client` or `@modelcontextprotocol/server` | -| `@modelcontextprotocol/sdk/shared/auth.js` | `@modelcontextprotocol/client` or `@modelcontextprotocol/server` | +| `@modelcontextprotocol/sdk/shared/auth.js` | Types / classes → `@modelcontextprotocol/client` or `@modelcontextprotocol/server`; OAuth/OpenID Zod `*Schema` constants (e.g. `OAuthTokensSchema`, `OAuthMetadataSchema`) → `@modelcontextprotocol/sdk-shared` | | `@modelcontextprotocol/sdk/shared/stdio.js` | `@modelcontextprotocol/client` or `@modelcontextprotocol/server` (`ReadBuffer`, `serializeMessage`, `deserializeMessage` are in the root barrel; the `./stdio` subpath only has the transport class) | Notes: diff --git a/docs/migration.md b/docs/migration.md index 8950d9150e..ce78605439 100644 --- a/docs/migration.md +++ b/docs/migration.md @@ -532,7 +532,7 @@ if (CallToolResultSchema.safeParse(value).success) { } ``` -`@modelcontextprotocol/sdk-shared` is the canonical home for the spec Zod schemas. `@modelcontextprotocol/server` and `@modelcontextprotocol/client` keep a Zod-free public surface, so the raw `*Schema` constants live in `sdk-shared`. (The codemod rewrites these imports for you.) +`@modelcontextprotocol/sdk-shared` is the canonical home for the Zod schema constants — both the spec schemas and the OAuth/OpenID schemas (e.g. `OAuthTokensSchema`, `OAuthMetadataSchema`) that v1 exported from `@modelcontextprotocol/sdk/shared/auth.js`. `@modelcontextprotocol/server` and `@modelcontextprotocol/client` keep a Zod-free public surface (they export the corresponding TypeScript types, e.g. `OAuthTokens`), so the raw `*Schema` constants live in `sdk-shared`. (The codemod rewrites these imports for you.) If you'd rather **not** depend on Zod, `@modelcontextprotocol/client` and `@modelcontextprotocol/server` also expose Zod-free validators keyed by `SpecTypeName` — a literal union of every named spec type, so you get autocomplete and a compile error on typos: diff --git a/packages/codemod/src/migrations/v1-to-v2/mappings/authSchemaNames.ts b/packages/codemod/src/migrations/v1-to-v2/mappings/authSchemaNames.ts new file mode 100644 index 0000000000..d643e87593 --- /dev/null +++ b/packages/codemod/src/migrations/v1-to-v2/mappings/authSchemaNames.ts @@ -0,0 +1,21 @@ +// The auth (OAuth / OpenID) Zod schema CONSTANTS that v1 exported from +// `@modelcontextprotocol/sdk/shared/auth.js` and that sdk-shared still re-exports in v2. The import +// transform routes a `*Schema` symbol imported from that v1 path to sdk-shared when its name is in this +// set (the corresponding TYPES, e.g. OAuthTokens, resolve by context to @modelcontextprotocol/client | +// /server). This is the v1 auth-schema set — a SUBSET of sdk-shared's auth exports. v2-only auth schemas +// (e.g. IdJagTokenExchangeResponseSchema) are exported by sdk-shared but NOT listed here: v1 never had +// them, so there is nothing to migrate. test/v1-to-v2/authSchemaNames.test.ts asserts every name here is +// exported by sdk-shared (so the rewritten import resolves). Keep alphabetized. +export const AUTH_SCHEMA_NAMES: ReadonlySet = new Set([ + 'OAuthClientInformationFullSchema', + 'OAuthClientInformationSchema', + 'OAuthClientMetadataSchema', + 'OAuthClientRegistrationErrorSchema', + 'OAuthErrorResponseSchema', + 'OAuthMetadataSchema', + 'OAuthProtectedResourceMetadataSchema', + 'OAuthTokenRevocationRequestSchema', + 'OAuthTokensSchema', + 'OpenIdProviderDiscoveryMetadataSchema', + 'OpenIdProviderMetadataSchema' +]); diff --git a/packages/codemod/src/migrations/v1-to-v2/mappings/importMap.ts b/packages/codemod/src/migrations/v1-to-v2/mappings/importMap.ts index d06d8f37ca..54e040c1ae 100644 --- a/packages/codemod/src/migrations/v1-to-v2/mappings/importMap.ts +++ b/packages/codemod/src/migrations/v1-to-v2/mappings/importMap.ts @@ -6,12 +6,13 @@ export interface ImportMapping { symbolTargetOverrides?: Record; /** * Route an imported symbol to this package (instead of `target`) when its rename-resolved name is - * an actual spec schema constant — a member of `SPEC_SCHEMA_NAMES`. Used for `sdk/types.js`: the - * spec Zod schemas now live in `@modelcontextprotocol/sdk-shared` (so `Schema.parse(...)` - * keeps working), while spec types/constants/guards resolve by context. Matching on membership - * (not a `*Schema` suffix) keeps spec TYPES whose name ends in `Schema` — e.g. the elicitation - * primitives `BooleanSchema`/`StringSchema`/`EnumSchema` — routed by context, where their types - * live. `symbolTargetOverrides` (exact-name) takes precedence. + * a Zod schema constant re-exported by sdk-shared — a member of `SPEC_SCHEMA_NAMES` (spec schemas, + * for `sdk/types.js`) or `AUTH_SCHEMA_NAMES` (OAuth/OpenID schemas, for `sdk/shared/auth.js`). The + * schemas now live in `@modelcontextprotocol/sdk-shared` (so `Schema.parse(...)` keeps + * working), while the corresponding types/constants/guards resolve by context. Matching on + * membership (not a `*Schema` suffix) keeps TYPES whose name ends in `Schema` — e.g. the + * elicitation primitives `BooleanSchema`/`StringSchema`/`EnumSchema` — routed by context, where + * their types live. `symbolTargetOverrides` (exact-name) takes precedence. */ schemaSymbolTarget?: string; removalMessage?: string; @@ -148,7 +149,12 @@ export const IMPORT_MAP: Record = { }, '@modelcontextprotocol/sdk/shared/auth.js': { target: 'RESOLVE_BY_CONTEXT', - status: 'moved' + status: 'moved', + // OAuth/OpenID Zod schema constants (AUTH_SCHEMA_NAMES) are re-exported by sdk-shared as a + // separate group, so route them there (keeping `OAuthTokensSchema.parse(...)` working). The + // OAuth/OpenID TYPES (OAuthTokens, etc.) carry no `schemaSymbolTarget` match and resolve by + // context to @modelcontextprotocol/client | /server. + schemaSymbolTarget: '@modelcontextprotocol/sdk-shared' }, '@modelcontextprotocol/sdk/shared/stdio.js': { target: 'RESOLVE_BY_CONTEXT', diff --git a/packages/codemod/src/migrations/v1-to-v2/transforms/importPaths.ts b/packages/codemod/src/migrations/v1-to-v2/transforms/importPaths.ts index 7ab0b724b7..61489a7187 100644 --- a/packages/codemod/src/migrations/v1-to-v2/transforms/importPaths.ts +++ b/packages/codemod/src/migrations/v1-to-v2/transforms/importPaths.ts @@ -7,6 +7,7 @@ import { actionRequired, info, v2Gap, warning } from '../../../utils/diagnostics import type { NamedImportSpec } from '../../../utils/importUtils.js'; import { addOrMergeImport, getSdkExports, getSdkImports, isTypeOnlyImport } from '../../../utils/importUtils.js'; import { resolveTypesPackage } from '../../../utils/projectAnalyzer.js'; +import { AUTH_SCHEMA_NAMES } from '../mappings/authSchemaNames.js'; import type { ImportMapping } from '../mappings/importMap.js'; import { isAuthImport, lookupImportMapping } from '../mappings/importMap.js'; import { SPEC_SCHEMA_NAMES } from '../mappings/specSchemaNames.js'; @@ -26,18 +27,28 @@ function resolveRenamedName(name: string, mapping: ImportMapping): string { return mapping.renamedSymbols?.[name] ?? SIMPLE_RENAMES[name] ?? name; } +/** + * True when `name` (after renames) is a Zod schema CONSTANT that sdk-shared re-exports — either a spec + * schema (`SPEC_SCHEMA_NAMES`) or an OAuth/OpenID schema (`AUTH_SCHEMA_NAMES`). Membership (not a + * `*Schema` suffix) is what keeps TYPES whose name ends in `Schema` — e.g. `BooleanSchema` — out. + */ +function isSharedSchemaConst(name: string, mapping: ImportMapping): boolean { + const resolved = resolveRenamedName(name, mapping); + return SPEC_SCHEMA_NAMES.has(resolved) || AUTH_SCHEMA_NAMES.has(resolved); +} + /** * The per-symbol target package for a symbol imported/re-exported from `mapping`'s module, or * `undefined` when the symbol should use the mapping's resolved `target`. Exact-name * `symbolTargetOverrides` win over `schemaSymbolTarget`, which routes a symbol to the shared-schemas - * package only when its rename-resolved name is an actual spec schema constant (`SPEC_SCHEMA_NAMES`) — - * not merely any name ending in `Schema`, so spec TYPES such as `BooleanSchema` resolve by context. + * package only when its rename-resolved name is a schema constant re-exported by sdk-shared (see + * `isSharedSchemaConst`). */ function symbolTargetOverride(name: string, mapping: ImportMapping): string | undefined { if (mapping.symbolTargetOverrides && name in mapping.symbolTargetOverrides) { return mapping.symbolTargetOverrides[name]; } - if (mapping.schemaSymbolTarget && SPEC_SCHEMA_NAMES.has(resolveRenamedName(name, mapping))) { + if (mapping.schemaSymbolTarget && isSharedSchemaConst(name, mapping)) { return mapping.schemaSymbolTarget; } return undefined; @@ -159,25 +170,29 @@ export const importPathsTransform: Transform = { // any `ns.Schema` accesses would silently resolve against the wrong package. Flag them. if (namespaceImport && mapping.schemaSymbolTarget) { const nsName = namespaceImport.getText(); - const schemaNames = [ - ...new Set( + // Map each accessed v1 name to the v2 name sdk-shared actually exports — some are + // renamed (e.g. JSONRPCErrorSchema → JSONRPCErrorResponseSchema), and sdk-shared only + // exports the v2 name. Dedupe by the accessed (v1) name. + const schemaAccesses = [ + ...new Map( sourceFile .getDescendantsOfKind(SyntaxKind.PropertyAccessExpression) - .filter( - pa => - pa.getExpression().getText() === nsName && - SPEC_SCHEMA_NAMES.has(resolveRenamedName(pa.getName(), mapping)) - ) - .map(pa => pa.getName()) + .filter(pa => pa.getExpression().getText() === nsName && isSharedSchemaConst(pa.getName(), mapping)) + .map(pa => [pa.getName(), resolveRenamedName(pa.getName(), mapping)] as const) ) ]; - if (schemaNames.length > 0) { + if (schemaAccesses.length > 0) { + const accessed = schemaAccesses.map(([v1]) => v1).join(', '); + const importName = schemaAccesses[0]![1]; + const renamed = schemaAccesses.filter(([v1, v2]) => v1 !== v2); + const renameNote = + renamed.length > 0 ? ` Renamed in v2: ${renamed.map(([v1, v2]) => `${v1} → ${v2}`).join(', ')}.` : ''; diagnostics.push( actionRequired( filePath, imp, - `Namespace import of ${specifier} is used to access Zod schema(s) (${schemaNames.join(', ')}) that moved to ${mapping.schemaSymbolTarget}. ` + - `Import them with a named import (e.g. \`import { ${schemaNames[0]} } from '${mapping.schemaSymbolTarget}'\`) and update the qualified usages.` + `Namespace import of ${specifier} is used to access Zod schema(s) (${accessed}) that moved to ${mapping.schemaSymbolTarget}.${renameNote} ` + + `Import them with a named import (e.g. \`import { ${importName} } from '${mapping.schemaSymbolTarget}'\`) and update the qualified usages.` ) ); } diff --git a/packages/codemod/src/migrations/v1-to-v2/transforms/schemaParamRemoval.ts b/packages/codemod/src/migrations/v1-to-v2/transforms/schemaParamRemoval.ts index 2e9737d383..719a4e261b 100644 --- a/packages/codemod/src/migrations/v1-to-v2/transforms/schemaParamRemoval.ts +++ b/packages/codemod/src/migrations/v1-to-v2/transforms/schemaParamRemoval.ts @@ -2,7 +2,7 @@ import type { SourceFile } from 'ts-morph'; import { Node, SyntaxKind } from 'ts-morph'; import type { Transform, TransformContext, TransformResult } from '../../../types.js'; -import { isImportedFromMcp, removeUnusedImport, resolveOriginalImportName } from '../../../utils/importUtils.js'; +import { hasMcpImports, isImportedFromMcp, removeUnusedImport, resolveOriginalImportName } from '../../../utils/importUtils.js'; const TARGET_METHODS = new Set(['request', 'callTool']); @@ -12,6 +12,11 @@ export const schemaParamRemovalTransform: Transform = { apply(sourceFile: SourceFile, _context: TransformContext): TransformResult { let changesCount = 0; + // `request`/`callTool` are common method names on non-MCP receivers too. The schema-identifier + // path guards per-symbol via `isImportedFromMcp`; the `undefined` path has no symbol to check, so + // gate it on a file-level MCP signal to avoid rewriting unrelated calls. + const fileHasMcpImports = hasMcpImports(sourceFile); + const calls = sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression); for (const call of calls) { @@ -33,7 +38,7 @@ export const schemaParamRemovalTransform: Transform = { // argument too many (TS2554). Drop it only when a third argument follows — a 2-arg // `callTool(params, undefined)` already type-checks, since `undefined` is a valid options arg. if (secondArg.getText() === 'undefined') { - if (args.length >= 3) { + if (fileHasMcpImports && args.length >= 3) { call.removeArgument(1); changesCount++; } diff --git a/packages/codemod/test/v1-to-v2/authSchemaNames.test.ts b/packages/codemod/test/v1-to-v2/authSchemaNames.test.ts new file mode 100644 index 0000000000..cb8f0f718c --- /dev/null +++ b/packages/codemod/test/v1-to-v2/authSchemaNames.test.ts @@ -0,0 +1,27 @@ +import { readFileSync } from 'node:fs'; +import { fileURLToPath } from 'node:url'; + +import { describe, expect, it } from 'vitest'; + +import { AUTH_SCHEMA_NAMES } from '../../src/migrations/v1-to-v2/mappings/authSchemaNames.js'; + +describe('AUTH_SCHEMA_NAMES (codemod auth schema-routing allowlist)', () => { + it('routes only auth schemas that @modelcontextprotocol/sdk-shared exports (drift guard)', () => { + // The import transform routes a `*Schema` symbol from sdk/shared/auth.js to sdk-shared only when + // its name is in AUTH_SCHEMA_NAMES, so EVERY name here MUST be exported by sdk-shared — otherwise + // the rewritten import would have no exported member. AUTH_SCHEMA_NAMES is the v1 auth-schema set, + // a SUBSET of sdk-shared's auth exports: sdk-shared may export more (v2-only schemas such as + // IdJagTokenExchangeResponseSchema) that v1 never had and the codemod never encounters. Read + // sdk-shared's barrel directly (the `export { … } from '…/core/auth'` block) so they cannot drift. + const src = readFileSync(fileURLToPath(new URL('../../../sdk-shared/src/index.ts', import.meta.url)), 'utf8'); + const closeIdx = src.indexOf("} from '@modelcontextprotocol/core/auth'"); + const openIdx = src.lastIndexOf('export {', closeIdx); + const block = src.slice(openIdx + 'export {'.length, closeIdx); + const sdkSharedAuthExports = new Set([...block.matchAll(/\b(\w+Schema)\b/g)].map(m => m[1])); + + const notExportedBySdkShared = [...AUTH_SCHEMA_NAMES].filter(name => !sdkSharedAuthExports.has(name)); + expect(notExportedBySdkShared).toEqual([]); + // The v1 auth-schema set is frozen; pin its size so an accidental add/remove is caught. + expect(AUTH_SCHEMA_NAMES.size).toBe(11); + }); +}); diff --git a/packages/codemod/test/v1-to-v2/transforms/importPaths.test.ts b/packages/codemod/test/v1-to-v2/transforms/importPaths.test.ts index 326579fe79..cf51484229 100644 --- a/packages/codemod/test/v1-to-v2/transforms/importPaths.test.ts +++ b/packages/codemod/test/v1-to-v2/transforms/importPaths.test.ts @@ -170,6 +170,23 @@ describe('import-paths transform', () => { expect(result).not.toContain('@modelcontextprotocol/sdk/'); }); + it('routes OAuth *Schema from sdk/shared/auth.js to sdk-shared; the TYPE resolves by context', () => { + // OAuthTokensSchema is a Zod schema re-exported by sdk-shared (AUTH_SCHEMA_NAMES), so route it + // there — `OAuthTokensSchema.parse(...)` keeps working. OAuthTokens (the type) has no schema-name + // match and resolves by context to @modelcontextprotocol/client. + const input = [ + `import { OAuthTokensSchema, OAuthTokens } from '@modelcontextprotocol/sdk/shared/auth.js';`, + `const t = OAuthTokensSchema.parse(raw);`, + `let x: OAuthTokens;`, + '' + ].join('\n'); + const result = applyTransform(input, { projectType: 'client' }); + expect(result).toMatch(/import\s*\{[^}]*\bOAuthTokensSchema\b[^}]*\}\s*from\s*["']@modelcontextprotocol\/sdk-shared["']/); + expect(result).toMatch(/import\s*\{[^}]*\bOAuthTokens\b[^}]*\}\s*from\s*["']@modelcontextprotocol\/client["']/); + expect(result).toContain('OAuthTokensSchema.parse(raw)'); + expect(result).not.toContain('@modelcontextprotocol/sdk/shared/auth'); + }); + it('does not rewrite schema .parse() usages (migrates as an import-path swap)', () => { const input = [ `import { CallToolResultSchema } from '@modelcontextprotocol/sdk/types.js';`, @@ -258,6 +275,23 @@ describe('import-paths transform', () => { expect(sourceFile.getFullText()).toContain('@modelcontextprotocol/server'); }); + it('suggests the v2 (rename-resolved) name in the namespace schema-access diagnostic', () => { + // JSONRPCErrorSchema is re-exported by sdk-shared as JSONRPCErrorResponseSchema; the suggested + // import must use the v2 name (the v1 name has no exported member), and mention the rename. + const input = [ + `import * as types from '@modelcontextprotocol/sdk/types.js';`, + `const r = types.JSONRPCErrorSchema.safeParse(value);`, + '' + ].join('\n'); + const project = new Project({ useInMemoryFileSystem: true }); + const sourceFile = project.createSourceFile('test.ts', input); + const result = importPathsTransform.apply(sourceFile, { projectType: 'server' }); + const msg = result.diagnostics.map(d => d.message).join('\n'); + expect(msg).toContain("import { JSONRPCErrorResponseSchema } from '@modelcontextprotocol/sdk-shared'"); + expect(msg).toContain('JSONRPCErrorSchema → JSONRPCErrorResponseSchema'); + expect(msg).not.toContain('import { JSONRPCErrorSchema } from'); + }); + it('does not flag a namespace import of sdk/types.js that only accesses types', () => { const input = [`import * as types from '@modelcontextprotocol/sdk/types.js';`, `const t: types.CallToolResult = value;`, ''].join( '\n' diff --git a/packages/codemod/test/v1-to-v2/transforms/schemaParamRemoval.test.ts b/packages/codemod/test/v1-to-v2/transforms/schemaParamRemoval.test.ts index 3547589e0c..c77ad9e7d9 100644 --- a/packages/codemod/test/v1-to-v2/transforms/schemaParamRemoval.test.ts +++ b/packages/codemod/test/v1-to-v2/transforms/schemaParamRemoval.test.ts @@ -120,21 +120,32 @@ describe('schema-param-removal transform', () => { it('removes a literal undefined schema slot from callTool when an options argument follows', () => { const input = [ + `import { Client } from '@modelcontextprotocol/sdk/client/index.js';`, `const result = await client.callTool({ name: 'add', arguments: { a: 1 } }, undefined, { onprogress: cb });`, '' ].join('\n'); const result = applyTransform(input); expect(result).toContain("client.callTool({ name: 'add', arguments: { a: 1 } }, { onprogress: cb })"); - expect(result).not.toContain('undefined'); + expect(result).not.toContain(', undefined,'); }); it('removes a literal undefined schema slot from request when an options argument follows', () => { - const input = [`const result = await client.request({ method: 'tools/call', params: {} }, undefined, { timeout: 5000 });`, ''].join( - '\n' - ); + const input = [ + `import { Client } from '@modelcontextprotocol/sdk/client/index.js';`, + `const result = await client.request({ method: 'tools/call', params: {} }, undefined, { timeout: 5000 });`, + '' + ].join('\n'); const result = applyTransform(input); expect(result).toContain("client.request({ method: 'tools/call', params: {} }, { timeout: 5000 })"); - expect(result).not.toContain('undefined'); + expect(result).not.toContain(', undefined,'); + }); + + it('does not strip undefined from request()/callTool() in a file with no MCP imports', () => { + // `request`/`callTool` are common non-MCP method names; without an MCP signal in the file the + // codemod must not touch them, or it would shift `someHttpClient.request(payload, undefined, opts)`. + const input = [`const r = await someHttpClient.request(payload, undefined, { timeout: 5000 });`, ''].join('\n'); + const result = applyTransform(input); + expect(result).toContain('someHttpClient.request(payload, undefined, { timeout: 5000 })'); }); it('leaves a 2-arg callTool(params, undefined) unchanged (already valid as options in v2)', () => { diff --git a/packages/core/src/exports/public/index.ts b/packages/core/src/exports/public/index.ts index ec0be8986c..82c1824195 100644 --- a/packages/core/src/exports/public/index.ts +++ b/packages/core/src/exports/public/index.ts @@ -19,6 +19,7 @@ export { SdkError, SdkErrorCode, SdkHttpError } from '../../errors/sdkErrors.js' // Auth TypeScript types (NOT Zod schemas like OAuthMetadataSchema) export type { AuthorizationServerMetadata, + IdJagTokenExchangeResponse, OAuthClientInformation, OAuthClientInformationFull, OAuthClientInformationMixed, diff --git a/packages/core/src/types/specTypeSchema.ts b/packages/core/src/types/specTypeSchema.ts index e538da8fa5..9eef4092d2 100644 --- a/packages/core/src/types/specTypeSchema.ts +++ b/packages/core/src/types/specTypeSchema.ts @@ -1,6 +1,7 @@ import type * as z from 'zod/v4'; import { + IdJagTokenExchangeResponseSchema, OAuthClientInformationFullSchema, OAuthClientInformationSchema, OAuthClientMetadataSchema, @@ -188,6 +189,7 @@ const SPEC_SCHEMA_KEYS = [ ] as const satisfies readonly (keyof typeof schemas)[]; const authSchemas = { + IdJagTokenExchangeResponseSchema, OAuthClientInformationFullSchema, OAuthClientInformationSchema, OAuthClientMetadataSchema, diff --git a/packages/core/test/types/specTypeSchema.test.ts b/packages/core/test/types/specTypeSchema.test.ts index 198e104f9f..03fc714ab2 100644 --- a/packages/core/test/types/specTypeSchema.test.ts +++ b/packages/core/test/types/specTypeSchema.test.ts @@ -166,10 +166,11 @@ describe('SPEC_SCHEMA_KEYS allowlist', () => { .filter(k => !INTERNAL_HELPER_SCHEMAS.includes(k)) .map(k => k.slice(0, -'Schema'.length)) .sort(); - // Auth schemas are sourced from shared/auth.ts, not schemas.ts, so filter them out of the - // observed side before comparing. + // Auth schemas are sourced from shared/auth.ts, not schemas.ts. Keep only the protocol entries + // (whose `*Schema` const lives in schemas.ts) so the comparison stays against schemas.ts — + // robust to new auth schemas (e.g. IdJagTokenExchangeResponse) without a name-prefix heuristic. const actual = Object.keys(isSpecType) - .filter(k => !k.startsWith('OAuth') && !k.startsWith('OpenId')) + .filter(k => `${k}Schema` in schemas) .sort(); expect(actual).toEqual(expected); }); diff --git a/packages/sdk-shared/src/index.ts b/packages/sdk-shared/src/index.ts index 25745b3b9f..ed0b62fbe9 100644 --- a/packages/sdk-shared/src/index.ts +++ b/packages/sdk-shared/src/index.ts @@ -1,21 +1,23 @@ // @modelcontextprotocol/sdk-shared // -// Canonical public home for the Model Context Protocol specification Zod schemas. +// Canonical public home for the Model Context Protocol specification + OAuth/OpenID Zod schemas. // // These are the exact schema constants the SDK validates against internally (defined in the // private @modelcontextprotocol/core package). This package bundles core and re-exports ONLY the -// spec `*Schema` Zod values, so consumers can validate protocol payloads directly — e.g. +// `*Schema` Zod values, so consumers can validate protocol/OAuth payloads directly — e.g. // `CallToolResultSchema.parse(value)` / `.safeParse(value)` — without depending on core's // internal barrel. // // Scope: Zod schemas ONLY. The corresponding spec TypeScript types, error classes, enums, and // type guards are part of the public API of @modelcontextprotocol/server and /client. // -// The list below is the spec `*Schema` set: every `export const *Schema` in core's schema module -// EXCEPT internal helper schemas that have no public spec type (e.g. BaseRequestParamsSchema, -// NotificationsParamsSchema). It mirrors core's SPEC_SCHEMA_KEYS allowlist; the sdkSharedSchemas -// test asserts it stays in sync. The @modelcontextprotocol/core specifier is aliased (tsconfig.json -// + tsdown.config.ts) to core's schemas module and bundled. +// Two groups, kept separate to mirror core's own spec-vs-auth split, each bundled from a build-only +// subpath alias of core (tsconfig.json + tsdown.config.ts): +// - SPEC schemas, from @modelcontextprotocol/core/schemas (core/src/types/schemas.ts): every +// `export const *Schema` EXCEPT internal helpers with no public spec type (e.g. +// BaseRequestParamsSchema). Mirrors core's SPEC_SCHEMA_KEYS allowlist. +// - OAUTH/OPENID schemas, from @modelcontextprotocol/core/auth (core/src/shared/auth.ts). +// The sdkSharedSchemas test asserts both groups stay in sync with their core source modules. export { AnnotationsSchema, AudioContentSchema, @@ -171,4 +173,26 @@ export { UnsubscribeRequestSchema, UntitledMultiSelectEnumSchemaSchema, UntitledSingleSelectEnumSchemaSchema -} from '@modelcontextprotocol/core'; +} from '@modelcontextprotocol/core/schemas'; + +// Auth schemas (OAuth / OpenID / IdJag) — kept as a SEPARATE group from the MCP spec schemas above, +// mirroring core's own spec-vs-auth split (these live in core/src/shared/auth.ts, not types/schemas.ts, +// and are registered as `authSchemas` in core's specTypeSchema.ts). This group is EXACTLY core's +// `authSchemas` set — every auth schema that has a public spec type (so `isSpecType.OAuthTokens`, +// `isSpecType.IdJagTokenExchangeResponse`, etc. exist). The typeless internal URL field-validators +// (SafeUrlSchema, OptionalSafeUrlSchema) are not auth schemas and stay out. The sdkSharedSchemas test +// asserts this group stays in sync with core's `authSchemas`. +export { + IdJagTokenExchangeResponseSchema, + OAuthClientInformationFullSchema, + OAuthClientInformationSchema, + OAuthClientMetadataSchema, + OAuthClientRegistrationErrorSchema, + OAuthErrorResponseSchema, + OAuthMetadataSchema, + OAuthProtectedResourceMetadataSchema, + OAuthTokenRevocationRequestSchema, + OAuthTokensSchema, + OpenIdProviderDiscoveryMetadataSchema, + OpenIdProviderMetadataSchema +} from '@modelcontextprotocol/core/auth'; diff --git a/packages/sdk-shared/test/sdkSharedSchemas.test.ts b/packages/sdk-shared/test/sdkSharedSchemas.test.ts index 7dc4e93e4d..cc7d3fa0be 100644 --- a/packages/sdk-shared/test/sdkSharedSchemas.test.ts +++ b/packages/sdk-shared/test/sdkSharedSchemas.test.ts @@ -4,38 +4,57 @@ import { fileURLToPath } from 'node:url'; import { describe, expect, it } from 'vitest'; import * as sdkShared from '../src/index.js'; -import { CursorSchema, InitializeRequestSchema } from '../src/index.js'; +import { CursorSchema, InitializeRequestSchema, OAuthTokensSchema } from '../src/index.js'; + +function readCore(relativePath: string): string { + return readFileSync(fileURLToPath(new URL(relativePath, import.meta.url)), 'utf8'); +} + +function exportedSchemaConsts(src: string, re: RegExp): string[] { + return [...src.matchAll(re)].map(m => m[1]).filter((name): name is string => name !== undefined && /^[A-Z]/.test(name)); +} describe('@modelcontextprotocol/sdk-shared', () => { - it('re-exports spec schemas as working Zod objects', () => { - // Round-trips a valid value and rejects an invalid one — proves the re-exports are the - // real Zod schemas (not type-only aliases) and that `.parse`/`.safeParse` work. + it('re-exports spec + OAuth schemas as working Zod objects', () => { + // Round-trips valid/invalid values — proves the re-exports are real Zod schemas (not type-only + // aliases) and that `.parse`/`.safeParse` work, for both the spec and the OAuth group. expect(CursorSchema.parse('abc')).toBe('abc'); expect(InitializeRequestSchema.safeParse({}).success).toBe(false); + expect(OAuthTokensSchema.safeParse({}).success).toBe(false); + expect(OAuthTokensSchema.safeParse({ access_token: 'tok', token_type: 'Bearer' }).success).toBe(true); }); - it('re-exports exactly the spec schemas declared in core — no internal helpers (drift guard)', () => { - // sdk-shared's public surface is the spec `*Schema` constants ONLY. Some `*Schema` consts in - // core's schemas.ts are internal building blocks with no public spec type; they must NOT leak - // here. This list mirrors the exclusion in core's specTypeSchema.ts (SPEC_SCHEMA_KEYS) — keep - // the two in sync. - const INTERNAL_HELPER_SCHEMAS = [ + it('re-exports exactly core’s spec + OAuth schemas — no internal helpers (drift guard)', () => { + // sdk-shared's public surface is two SEPARATE groups, mirroring core's own spec-vs-auth split: + // 1. spec `*Schema` constants from core/src/types/schemas.ts (minus internal helpers with no + // public spec type — they must NOT leak), mirroring core's SPEC_SCHEMA_KEYS allowlist; and + // 2. the auth `*Schema` constants registered in core's `authSchemas` object (specTypeSchema.ts) + // — i.e. the auth schemas that have a public spec type. Reading that object directly (not a + // name prefix) is the source of truth, so a new auth schema added to core is required here + // automatically; typeless internal helpers (SafeUrlSchema, OptionalSafeUrlSchema) stay out + // because they are not in `authSchemas`. + // Read the core sources directly so the groups cannot silently drift. + const SPEC_INTERNAL_HELPERS = [ 'BaseRequestParamsSchema', 'ClientTasksCapabilitySchema', 'ListChangedOptionsBaseSchema', 'NotificationsParamsSchema', 'ServerTasksCapabilitySchema' ]; - const src = readFileSync(fileURLToPath(new URL('../../core/src/types/schemas.ts', import.meta.url)), 'utf8'); - const coreSchemas = [...src.matchAll(/^export const (\w+Schema)\b/gm)] - .map(m => m[1]) - .filter((name): name is string => name !== undefined && /^[A-Z]/.test(name)); - // The spec schema set = every PascalCase core `*Schema` const minus the internal helpers. - const specSchemas = coreSchemas.filter(name => !INTERNAL_HELPER_SCHEMAS.includes(name)).sort(); + const specSchemas = exportedSchemaConsts(readCore('../../core/src/types/schemas.ts'), /^export const (\w+Schema)\b/gm).filter( + name => !SPEC_INTERNAL_HELPERS.includes(name) + ); + const specTypeSrc = readCore('../../core/src/types/specTypeSchema.ts'); + const authStart = specTypeSrc.indexOf('const authSchemas = {'); + const authObj = specTypeSrc.slice(authStart, specTypeSrc.indexOf('} as const', authStart)); + const authSchemas = exportedSchemaConsts(authObj, /\b(\w+Schema)\b/g); + + const expected = [...specSchemas, ...authSchemas].sort(); const exported = Object.keys(sdkShared).sort(); - // Exact match, both directions: a new core spec schema missing here fails (we forgot to + // Exact match, both directions: a new core spec/auth schema missing here fails (we forgot to // re-export it), and any internal helper / non-spec symbol that leaks here also fails. - expect(exported).toEqual(specSchemas); + expect(exported).toEqual(expected); expect(specSchemas.length).toBeGreaterThanOrEqual(154); + expect(authSchemas.length).toBe(12); }); }); diff --git a/packages/sdk-shared/tsconfig.json b/packages/sdk-shared/tsconfig.json index 7a1fa16622..f75f697009 100644 --- a/packages/sdk-shared/tsconfig.json +++ b/packages/sdk-shared/tsconfig.json @@ -5,7 +5,8 @@ "compilerOptions": { "paths": { "*": ["./*"], - "@modelcontextprotocol/core": ["./node_modules/@modelcontextprotocol/core/src/types/schemas.ts"] + "@modelcontextprotocol/core/schemas": ["./node_modules/@modelcontextprotocol/core/src/types/schemas.ts"], + "@modelcontextprotocol/core/auth": ["./node_modules/@modelcontextprotocol/core/src/shared/auth.ts"] } } } diff --git a/packages/sdk-shared/tsdown.config.ts b/packages/sdk-shared/tsdown.config.ts index 7fe5a320db..0d55609f6b 100644 --- a/packages/sdk-shared/tsdown.config.ts +++ b/packages/sdk-shared/tsdown.config.ts @@ -1,11 +1,14 @@ import { defineConfig } from 'tsdown'; -// sdk-shared re-exports ONLY the spec Zod schemas from @modelcontextprotocol/core (private, -// unpublished). The core specifier is aliased to core's schemas module (core/src/types/schemas.ts) -// rather than its barrel, so the bundled graph is just the schemas + the constants they use — -// never Protocol, transports, stdio, or the ajv/cfWorker validators. `platform: 'neutral'` keeps -// the output runtime-neutral: a node-only dependency leaking into the graph would fail the build -// here instead of silently shipping. +// sdk-shared re-exports ONLY the spec + OAuth Zod schemas from @modelcontextprotocol/core (private, +// unpublished). Two BUILD-ONLY subpath aliases (not real core exports) point at core's two schema +// modules, kept as separate sources: +// @modelcontextprotocol/core/schemas → core/src/types/schemas.ts (MCP spec schemas) +// @modelcontextprotocol/core/auth → core/src/shared/auth.ts (OAuth/OpenID schemas) +// Aliasing to these modules rather than core's barrel keeps the bundled graph to just the schemas + +// the constants they use — never Protocol, transports, stdio, or the ajv/cfWorker validators. Both +// modules import only `zod/v4`, so the graph stays runtime-neutral; `platform: 'neutral'` makes a +// node-only dependency leaking in fail the build here instead of silently shipping. export default defineConfig({ failOnWarn: 'ci-only', entry: ['src/index.ts'], @@ -20,9 +23,10 @@ export default defineConfig({ compilerOptions: { baseUrl: '.', paths: { - '@modelcontextprotocol/core': ['../core/src/types/schemas.ts'] + '@modelcontextprotocol/core/schemas': ['../core/src/types/schemas.ts'], + '@modelcontextprotocol/core/auth': ['../core/src/shared/auth.ts'] } } }, - noExternal: ['@modelcontextprotocol/core'] + noExternal: ['@modelcontextprotocol/core/schemas', '@modelcontextprotocol/core/auth'] }); From ac39021dc2e2064591ea7a47db69f4652d45510d Mon Sep 17 00:00:00 2001 From: Konstantin Konstantinov Date: Thu, 25 Jun 2026 11:38:18 +0300 Subject: [PATCH 10/21] fixes --- .changeset/add-sdk-shared-package.md | 2 +- .changeset/codemod-sdk-shared-routing.md | 2 +- .changeset/idjag-spec-type-export.md | 6 +++++ .../v1-to-v2/transforms/importPaths.ts | 12 ++++++--- .../v1-to-v2/transforms/importPaths.test.ts | 26 +++++++++++++++++++ 5 files changed, 42 insertions(+), 6 deletions(-) create mode 100644 .changeset/idjag-spec-type-export.md diff --git a/.changeset/add-sdk-shared-package.md b/.changeset/add-sdk-shared-package.md index 48cc1291e1..e9a88426e9 100644 --- a/.changeset/add-sdk-shared-package.md +++ b/.changeset/add-sdk-shared-package.md @@ -2,4 +2,4 @@ '@modelcontextprotocol/sdk-shared': minor --- -Add `@modelcontextprotocol/sdk-shared`: the public home for the MCP specification Zod schemas. It bundles the SDK's internal schema definitions and re-exports only the `*Schema` values, so consumers can validate protocol payloads (`Schema.parse(value)` / `.safeParse(value)`) without depending on a package's internal barrel. Spec types, error classes, enums, and guards continue to live on `@modelcontextprotocol/server` and `@modelcontextprotocol/client`. +Add `@modelcontextprotocol/sdk-shared`: the public home for the MCP specification and OAuth/OpenID Zod schemas. It bundles the SDK's internal schema definitions and re-exports only the `*Schema` values, so consumers can validate protocol payloads (`Schema.parse(value)` / `.safeParse(value)`) without depending on a package's internal barrel. Alongside the spec schemas it also re-exports the auth schemas v1 exposed from `@modelcontextprotocol/sdk/shared/auth.js` (e.g. `OAuthTokensSchema`, `OAuthMetadataSchema`, `IdJagTokenExchangeResponseSchema`). Spec types, error classes, enums, and guards continue to live on `@modelcontextprotocol/server` and `@modelcontextprotocol/client`. diff --git a/.changeset/codemod-sdk-shared-routing.md b/.changeset/codemod-sdk-shared-routing.md index 9b9dddfbaf..2563314304 100644 --- a/.changeset/codemod-sdk-shared-routing.md +++ b/.changeset/codemod-sdk-shared-routing.md @@ -2,4 +2,4 @@ '@modelcontextprotocol/codemod': minor --- -Route v1 `@modelcontextprotocol/sdk/types.js` schema imports to the new `@modelcontextprotocol/sdk-shared` package. The `*Schema` Zod constants now migrate as a behavior-preserving import-path swap — `Schema.parse(value)` / `.safeParse(value)` keep working — while spec types, error classes, enums, and guards continue to resolve to `@modelcontextprotocol/client` / `@modelcontextprotocol/server` by context. A single `import { CallToolResult, CallToolResultSchema } from '.../types.js'` is split accordingly. The previous `specSchemaAccess` transform (which rewrote `.parse()` into `specTypeSchemas.X['~standard'].validate(...)`) is removed. +Route v1 `@modelcontextprotocol/sdk/types.js` schema imports to the new `@modelcontextprotocol/sdk-shared` package. The `*Schema` Zod constants now migrate as a behavior-preserving import-path swap — `Schema.parse(value)` / `.safeParse(value)` keep working — while spec types, error classes, enums, and guards continue to resolve to `@modelcontextprotocol/client` / `@modelcontextprotocol/server` by context. A single `import { CallToolResult, CallToolResultSchema } from '.../types.js'` is split accordingly. The v1 OAuth/OpenID `*Schema` constants imported from `@modelcontextprotocol/sdk/shared/auth.js` are routed to `sdk-shared` the same way (their auth TYPES keep resolving to `client` / `server`). The previous `specSchemaAccess` transform (which rewrote `.parse()` into `specTypeSchemas.X['~standard'].validate(...)`) is removed. diff --git a/.changeset/idjag-spec-type-export.md b/.changeset/idjag-spec-type-export.md new file mode 100644 index 0000000000..e3c9e05a03 --- /dev/null +++ b/.changeset/idjag-spec-type-export.md @@ -0,0 +1,6 @@ +--- +'@modelcontextprotocol/client': minor +'@modelcontextprotocol/server': minor +--- + +Add the v2 `IdJagTokenExchangeResponse` type to the public API and register its schema as an MCP spec type. `IdJagTokenExchangeResponse` is now exported from `@modelcontextprotocol/client` and `@modelcontextprotocol/server`, `'IdJagTokenExchangeResponse'` joins the `SpecTypeName` union, and `isSpecType.IdJagTokenExchangeResponse(value)` / `specTypeSchemas.IdJagTokenExchangeResponse` validate it by name — matching how the OAuth/OpenID auth schemas are already exposed. The Zod schema itself, `IdJagTokenExchangeResponseSchema`, is available from `@modelcontextprotocol/sdk-shared`. diff --git a/packages/codemod/src/migrations/v1-to-v2/transforms/importPaths.ts b/packages/codemod/src/migrations/v1-to-v2/transforms/importPaths.ts index 61489a7187..35f14fc40e 100644 --- a/packages/codemod/src/migrations/v1-to-v2/transforms/importPaths.ts +++ b/packages/codemod/src/migrations/v1-to-v2/transforms/importPaths.ts @@ -90,10 +90,14 @@ export const importPathsTransform: Transform = { // A leading file-header / JSDoc comment attaches to the first SDK import as leading trivia. When // that import is removed and re-emitted (the per-symbol split/merge path calls imp.remove()), // ts-morph drops the comment with it. Capture it now and restore it after emitting if it was lost. - const leadingCommentText = sdkImports[0]! - .getLeadingCommentRanges() - .map(r => r.getText()) - .join('\n'); + // Capture the EXACT source bytes spanning all leading comment ranges (first range's start to last + // range's end) rather than re-joining each range with '\n' — a join drops the original separators + // (a blank line, or CRLF in CRLF files), so the later survival check would never match a header + // that actually survived (in-place setModuleSpecifier rewrite) and would re-insert it, duplicating + // it. The slice reproduces the block verbatim, so the includes() guard below is byte-exact. + const leadingRanges = sdkImports[0]!.getLeadingCommentRanges(); + const leadingCommentText = + leadingRanges.length > 0 ? sourceFile.getFullText().slice(leadingRanges[0]!.getPos(), leadingRanges.at(-1)!.getEnd()) : ''; interface PendingImport { specs: NamedImportSpec[]; diff --git a/packages/codemod/test/v1-to-v2/transforms/importPaths.test.ts b/packages/codemod/test/v1-to-v2/transforms/importPaths.test.ts index cf51484229..a118c1fe32 100644 --- a/packages/codemod/test/v1-to-v2/transforms/importPaths.test.ts +++ b/packages/codemod/test/v1-to-v2/transforms/importPaths.test.ts @@ -170,6 +170,32 @@ describe('import-paths transform', () => { expect(result).not.toContain('@modelcontextprotocol/sdk/'); }); + it('does not duplicate a multi-block leading header (blank line) when rewriting the first import in place', () => { + // The first SDK import is a namespace import, so it is rewritten in place (setModuleSpecifier) and + // its leading comments survive. The header is two // blocks separated by a BLANK line. A `\n`-join + // of the comment ranges loses that blank line, so the survival check would mis-fire and re-insert + // the header — duplicating it. The captured text must match the file's bytes exactly. + const input = [ + `// Copyright ACME`, + ``, + `// Notes about the types module`, + `import * as types from '@modelcontextprotocol/sdk/types.js';`, + '' + ].join('\n'); + const result = applyTransform(input, { projectType: 'server' }); + expect(result.split('// Copyright ACME').length - 1).toBe(1); + expect(result).toContain('@modelcontextprotocol/server'); + }); + + it('does not duplicate a CRLF leading header when rewriting the first import in place', () => { + // Same in-place rewrite, but the two // header lines are separated by CRLF. A `\n`-join never + // matches the file's `\r\n`, so the survival check would mis-fire and duplicate the header. + const input = `// Copyright ACME\r\n// Licensed MIT\r\n\r\nimport * as types from '@modelcontextprotocol/sdk/types.js';\r\n`; + const result = applyTransform(input, { projectType: 'server' }); + expect(result.split('// Copyright ACME').length - 1).toBe(1); + expect(result).toContain('@modelcontextprotocol/server'); + }); + it('routes OAuth *Schema from sdk/shared/auth.js to sdk-shared; the TYPE resolves by context', () => { // OAuthTokensSchema is a Zod schema re-exported by sdk-shared (AUTH_SCHEMA_NAMES), so route it // there — `OAuthTokensSchema.parse(...)` keeps working. OAuthTokens (the type) has no schema-name From 6d56a4999e924a1aefce984d5f396f150e8fe5b0 Mon Sep 17 00:00:00 2001 From: Konstantin Konstantinov Date: Thu, 25 Jun 2026 12:11:24 +0300 Subject: [PATCH 11/21] readme fix --- packages/sdk-shared/README.md | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/packages/sdk-shared/README.md b/packages/sdk-shared/README.md index 93ecf61690..3902f4db19 100644 --- a/packages/sdk-shared/README.md +++ b/packages/sdk-shared/README.md @@ -1,9 +1,9 @@ # @modelcontextprotocol/sdk-shared -Canonical public home for the [Model Context Protocol](https://modelcontextprotocol.io) specification **Zod schemas**. +Canonical public home for the [Model Context Protocol](https://modelcontextprotocol.io) specification and OAuth/OpenID **Zod schemas**. -These are the exact schema constants the SDK validates protocol payloads against internally. The `@modelcontextprotocol/server` and `@modelcontextprotocol/client` packages keep a Zod-free public surface, so this package exists as the supported place to import the raw schemas when -you need to validate or parse MCP messages yourself. +These are the exact schema constants the SDK validates protocol and OAuth/OpenID payloads against internally. The `@modelcontextprotocol/server` and `@modelcontextprotocol/client` packages keep a Zod-free public surface, so this package exists as the supported place to import the +raw schemas when you need to validate or parse MCP messages yourself. ## Install @@ -28,7 +28,13 @@ if (parsed.success) { ## Scope -This package exports **only** the spec Zod schemas (`*Schema`). The corresponding TypeScript types, error classes, enums, and type guards are part of the public API of [`@modelcontextprotocol/server`](https://www.npmjs.com/package/@modelcontextprotocol/server) and +This package exports **only** Zod schema constants (`*Schema`), in two groups: + +- the MCP **spec** schemas — `CallToolResultSchema`, `ListToolsResultSchema`, …; and +- the **OAuth/OpenID** auth schemas — `OAuthTokensSchema`, `OAuthMetadataSchema`, `IdJagTokenExchangeResponseSchema`, … (the schemas v1 exposed from `@modelcontextprotocol/sdk/shared/auth.js`). + +The corresponding TypeScript types, error classes, enums, and type guards are part of the public API of [`@modelcontextprotocol/server`](https://www.npmjs.com/package/@modelcontextprotocol/server) and [`@modelcontextprotocol/client`](https://www.npmjs.com/package/@modelcontextprotocol/client). -> **Migrating from v1?** In v1 these schemas were imported from `@modelcontextprotocol/sdk/types.js`. Point those `*Schema` imports at `@modelcontextprotocol/sdk-shared` and your existing `.parse()` / `.safeParse()` calls keep working unchanged. +> **Migrating from v1?** In v1 these schemas were imported from `@modelcontextprotocol/sdk/types.js` (spec schemas) and `@modelcontextprotocol/sdk/shared/auth.js` (OAuth/OpenID schemas). Point those `*Schema` imports at `@modelcontextprotocol/sdk-shared` and your existing +> `.parse()` / `.safeParse()` calls keep working unchanged. From 3f198f57ce6897bafc13e3c52445a3814e517b8f Mon Sep 17 00:00:00 2001 From: Konstantin Konstantinov Date: Thu, 25 Jun 2026 12:43:03 +0300 Subject: [PATCH 12/21] fixes --- .../v1-to-v2/transforms/importPaths.ts | 77 +++++++++++-------- .../v1-to-v2/transforms/importPaths.test.ts | 50 ++++++++++++ 2 files changed, 96 insertions(+), 31 deletions(-) diff --git a/packages/codemod/src/migrations/v1-to-v2/transforms/importPaths.ts b/packages/codemod/src/migrations/v1-to-v2/transforms/importPaths.ts index 35f14fc40e..57c1283055 100644 --- a/packages/codemod/src/migrations/v1-to-v2/transforms/importPaths.ts +++ b/packages/codemod/src/migrations/v1-to-v2/transforms/importPaths.ts @@ -143,15 +143,22 @@ export const importPathsTransform: Transform = { continue; } + // Resolve a RESOLVE_BY_CONTEXT mapping (sdk/types.js, sdk/shared/auth.js) only when a binding + // actually routes to the context package. resolveTypesPackage's diagnostic sink emits a "could + // not determine project type" warning (or, for a 'both' project, an info note), so resolving + // eagerly would emit that note even for an import of nothing but `*Schema` constants — which + // routes entirely to sdk-shared and never uses the context package. A namespace or default + // binding always needs context; a named symbol needs it only when it has no per-symbol override + // (i.e. it is not a `*Schema` routed to sdk-shared). let targetPackage = mapping.target; if (targetPackage === 'RESOLVE_BY_CONTEXT') { - targetPackage = resolveTypesPackage(context, hasClientImport, hasServerImport, { - filePath, - line, - diagnostics - }); - if (mapping.subpathSuffix) { - targetPackage = `${targetPackage}${mapping.subpathSuffix}`; + const needsContext = + namespaceImport != null || + defaultImport != null || + namedImports.some(n => symbolTargetOverride(n.getName(), mapping!) === undefined); + if (needsContext) { + const base = resolveTypesPackage(context, hasClientImport, hasServerImport, { filePath, line, diagnostics }); + targetPackage = mapping.subpathSuffix ? `${base}${mapping.subpathSuffix}` : base; } } @@ -165,14 +172,18 @@ export const importPathsTransform: Transform = { } } - // Default and namespace imports cannot be split per-symbol — the whole binding moves to one - // package. Named imports (aliased or not) fall through to the per-symbol splitter below, so a - // single aliased specifier no longer forces unrelated symbols into the wrong package. - if (defaultImport || namespaceImport) { + // A namespace import (`import * as ns from …`) cannot be split per-symbol — usages are + // qualified (`ns.Foo`), so the whole binding moves to one package. Named imports (aliased or + // not), including the named siblings of a default import, DO fall through to the per-symbol + // splitter below — so an all-`*Schema` import routes entirely to sdk-shared, a single aliased + // specifier no longer forces unrelated symbols into the wrong package, and a mixed + // `import sdk, { CallToolResultSchema }` routes the schema to sdk-shared while the default + // binding (handled at the end of the per-symbol path) moves to the context package. + if (namespaceImport) { const effectiveTarget = targetPackage; - // A namespace import (`import * as ns from '…/types.js'`) cannot be split per-symbol, so - // any `ns.Schema` accesses would silently resolve against the wrong package. Flag them. - if (namespaceImport && mapping.schemaSymbolTarget) { + // Any `ns.Schema` accesses would silently resolve against the wrong package (the + // namespace can't be split), so flag them. + if (mapping.schemaSymbolTarget) { const nsName = namespaceImport.getText(); // Map each accessed v1 name to the v2 name sdk-shared actually exports — some are // renamed (e.g. JSONRPCErrorSchema → JSONRPCErrorResponseSchema), and sdk-shared only @@ -204,22 +215,14 @@ export const importPathsTransform: Transform = { usedPackages.add(effectiveTarget); imp.setModuleSpecifier(effectiveTarget); if (mapping.renamedSymbols) { - for (const n of namedImports) { - const newName = mapping.renamedSymbols[n.getName()]; - if (newName) { - n.setName(newName); - } - } - if (namespaceImport) { - diagnostics.push( - actionRequired( - filePath, - imp, - `Namespace import of ${specifier}: exported symbol(s) ${Object.keys(mapping.renamedSymbols).join(', ')} ` + - `were renamed in ${effectiveTarget}. Update qualified accesses manually.` - ) - ); - } + diagnostics.push( + actionRequired( + filePath, + imp, + `Namespace import of ${specifier}: exported symbol(s) ${Object.keys(mapping.renamedSymbols).join(', ')} ` + + `were renamed in ${effectiveTarget}. Update qualified accesses manually.` + ) + ); } changesCount++; if (mapping.migrationHint) { @@ -240,7 +243,19 @@ export const importPathsTransform: Transform = { usedPackages.add(symbolTarget); addPending(symbolTarget, [alias ? { name: resolvedName, alias } : resolvedName], specifierTypeOnly); } - imp.remove(); + if (defaultImport) { + // The default binding can't be split per-symbol, so move it (and the module specifier) to + // the resolved context/target package. The named siblings were just routed per-symbol + // above, so drop them from this now default-only import. + const effectiveTarget = targetPackage; + usedPackages.add(effectiveTarget); + if (namedImports.length > 0) { + imp.removeNamedImports(); + } + imp.setModuleSpecifier(effectiveTarget); + } else { + imp.remove(); + } changesCount++; if (mapping.migrationHint) { diagnostics.push(info(filePath, line, mapping.migrationHint)); diff --git a/packages/codemod/test/v1-to-v2/transforms/importPaths.test.ts b/packages/codemod/test/v1-to-v2/transforms/importPaths.test.ts index a118c1fe32..29f0825c89 100644 --- a/packages/codemod/test/v1-to-v2/transforms/importPaths.test.ts +++ b/packages/codemod/test/v1-to-v2/transforms/importPaths.test.ts @@ -213,6 +213,56 @@ describe('import-paths transform', () => { expect(result).not.toContain('@modelcontextprotocol/sdk/shared/auth'); }); + it('does not emit a project-type note when every symbol routes to sdk-shared (both project)', () => { + // A types.js import of nothing but `*Schema` constants routes entirely to sdk-shared, so the + // context package is never used — resolveTypesPackage must not be called, and no "both"-project + // info note should be emitted. + const project = new Project({ useInMemoryFileSystem: true }); + const sourceFile = project.createSourceFile( + 'test.ts', + `import { CallToolResultSchema, ListToolsResultSchema } from '@modelcontextprotocol/sdk/types.js';\n` + ); + const result = importPathsTransform.apply(sourceFile, { projectType: 'both' }); + expect(result.diagnostics.some(d => /both client and server|determine project type/i.test(d.message))).toBe(false); + expect(sourceFile.getFullText()).toContain('@modelcontextprotocol/sdk-shared'); + expect(sourceFile.getFullText()).not.toContain('@modelcontextprotocol/sdk/types'); + }); + + it('does not warn about project type when an auth-schema-only import routes entirely to sdk-shared (unknown project)', () => { + const project = new Project({ useInMemoryFileSystem: true }); + const sourceFile = project.createSourceFile( + 'test.ts', + `import { OAuthTokensSchema, OAuthMetadataSchema } from '@modelcontextprotocol/sdk/shared/auth.js';\n` + ); + const result = importPathsTransform.apply(sourceFile, { projectType: 'unknown' }); + expect(result.diagnostics.some(d => /determine project type/i.test(d.message))).toBe(false); + expect(sourceFile.getFullText()).toContain('@modelcontextprotocol/sdk-shared'); + }); + + it('still warns about project type when a non-schema symbol falls through to context (unknown project)', () => { + // Control: `Tool` is a type with no schema-name match, so it falls through to context resolution — + // the warning must still fire (lazy resolution must not suppress genuine fall-throughs). + const project = new Project({ useInMemoryFileSystem: true }); + const sourceFile = project.createSourceFile( + 'test.ts', + `import { CallToolResultSchema, Tool } from '@modelcontextprotocol/sdk/types.js';\n` + ); + const result = importPathsTransform.apply(sourceFile, { projectType: 'unknown' }); + expect(result.diagnostics.some(d => /determine project type/i.test(d.message))).toBe(true); + }); + + it('splits a mixed default + named schema import — schema to sdk-shared, default to context', () => { + // The named `CallToolResultSchema` must route to sdk-shared even though a default import is present; + // the default binding (which can't be split) moves to the context package. Pre-fix the whole import + // moved to context and the schema silently became a "no exported member" error. + const result = applyTransform(`import sdk, { CallToolResultSchema } from '@modelcontextprotocol/sdk/types.js';\n`, { + projectType: 'server' + }); + expect(result).toMatch(/import\s*\{[^}]*\bCallToolResultSchema\b[^}]*\}\s*from\s*["']@modelcontextprotocol\/sdk-shared["']/); + expect(result).toMatch(/import\s+sdk\s+from\s*["']@modelcontextprotocol\/server["']/); + expect(result).not.toContain('@modelcontextprotocol/sdk/types'); + }); + it('does not rewrite schema .parse() usages (migrates as an import-path swap)', () => { const input = [ `import { CallToolResultSchema } from '@modelcontextprotocol/sdk/types.js';`, From 065e1421f9260febca117bc99a5f18612a9b4c66 Mon Sep 17 00:00:00 2001 From: Konstantin Konstantinov Date: Thu, 25 Jun 2026 13:20:32 +0300 Subject: [PATCH 13/21] fix(codemod): bound detectFormatter walk at /Users/kkonstantinov for non-git projects --- packages/codemod/src/utils/detectFormatter.ts | 29 +++++++++++----- packages/codemod/test/detectFormatter.test.ts | 34 +++++++++++++++++++ 2 files changed, 55 insertions(+), 8 deletions(-) diff --git a/packages/codemod/src/utils/detectFormatter.ts b/packages/codemod/src/utils/detectFormatter.ts index 29d584706a..81fc66da64 100644 --- a/packages/codemod/src/utils/detectFormatter.ts +++ b/packages/codemod/src/utils/detectFormatter.ts @@ -1,4 +1,5 @@ import { existsSync, readFileSync } from 'node:fs'; +import os from 'node:os'; import path from 'node:path'; /** A code formatter the codemod can recommend running after a migration. */ @@ -78,19 +79,27 @@ function readPackageJsonSignals(dir: string): PackageJsonSignals { } /** - * Walks up from `startDir` — bounded to the repository, stopping at a `.git` - * directory so a global config in `$HOME` is never matched — looking for a - * configured code formatter, so the CLI can suggest the right "format your - * changed files" command after a migration. + * Walks up from `startDir` looking for a configured code formatter, so the CLI can suggest the right + * "format your changed files" command after a migration. * - * Detection is config-based and runs nothing. When multiple formatters are - * configured, precedence is Biome > Prettier > ESLint. + * The walk is bounded so a user-level global config is never mistaken for the project's. It stops at the + * repository root (a `.git` directory) or the filesystem root, and — for a project that is not a git + * checkout (tarball, fresh scaffold, CI workspace) — never ascends into or above `$HOME`, so a + * `~/.prettierrc`, `~/biome.json`, or `~/package.json` with formatter deps is never matched. (A `.git` + * boundary alone did not hold this guarantee for non-git projects, which would otherwise walk to `$HOME`.) * + * Detection is config-based and runs nothing. When multiple formatters are configured, precedence is + * Biome > Prettier > ESLint. + * + * @param startDir the directory to start the upward search from. + * @param homeDir the user's home directory; the walk never reads it or any ancestor. Injectable for tests; + * defaults to `os.homedir()`. * @returns the detected formatter, or `null` if none is configured. */ -export function detectFormatter(startDir: string): DetectedFormatter | null { +export function detectFormatter(startDir: string, homeDir: string = os.homedir()): DetectedFormatter | null { let dir = path.resolve(startDir); const root = path.parse(dir).root; + const home = path.resolve(homeDir); const found = { biome: false, prettier: false, eslint: false }; while (true) { @@ -102,7 +111,11 @@ export function detectFormatter(startDir: string): DetectedFormatter | null { if (signals.prettier) found.prettier = true; if (signals.eslint) found.eslint = true; - if (existsSync(path.join(dir, '.git')) || dir === root) break; + // Stop at the repository root (a `.git` dir) or the filesystem root. For a project that is not a + // git checkout (tarball, fresh scaffold, CI workspace), also stop before ascending into `$HOME`: + // the project is a descendant of `$HOME`, so a user-level `~/.prettierrc`, `~/biome.json`, or + // `~/package.json` with formatter deps must never be read as the project's own config. + if (existsSync(path.join(dir, '.git')) || dir === root || dir === home || path.dirname(dir) === home) break; dir = path.dirname(dir); } diff --git a/packages/codemod/test/detectFormatter.test.ts b/packages/codemod/test/detectFormatter.test.ts index 4958815949..0b569a5e80 100644 --- a/packages/codemod/test/detectFormatter.test.ts +++ b/packages/codemod/test/detectFormatter.test.ts @@ -116,4 +116,38 @@ describe('detectFormatter', () => { expect(detectFormatter(src)).toBeNull(); }); + + it('does not match a user-level $HOME config for a non-git project (stops at $HOME)', () => { + const home = createTempDir(); + const projectSrc = path.join(home, 'projects', 'app', 'src'); + mkdirSync(projectSrc, { recursive: true }); + // A user-level config sits at $HOME, above a non-git project — it must never be read as the + // project's config (the walk must stop before ascending into $HOME). + writeFileSync(path.join(home, 'prettier.config.mjs'), 'export default {};'); + + expect(detectFormatter(projectSrc, home)).toBeNull(); + }); + + it('reads the project root directly under $HOME but never $HOME itself', () => { + const home = createTempDir(); + const projectRoot = path.join(home, 'app'); + const src = path.join(projectRoot, 'src'); + mkdirSync(src, { recursive: true }); + // A higher-precedence formatter configured at $HOME must NOT shadow the project's own. + writeFileSync(path.join(home, 'biome.json'), '{}'); + writeFileSync(path.join(projectRoot, 'eslint.config.js'), 'export default [];'); + + // Only the project's ESLint is detected; $HOME's Biome is never read. + expect(detectFormatter(src, home)?.name).toBe('ESLint'); + }); + + it('detects a project config below $HOME (the boundary only blocks $HOME and above)', () => { + const home = createTempDir(); + const projectRoot = path.join(home, 'projects', 'app'); + const src = path.join(projectRoot, 'src'); + mkdirSync(src, { recursive: true }); + writeFileSync(path.join(projectRoot, 'biome.json'), '{}'); + + expect(detectFormatter(src, home)?.name).toBe('Biome'); + }); }); From faab4bc8d837ba5a86b355c8c5e26b8414bb9d16 Mon Sep 17 00:00:00 2001 From: Konstantin Konstantinov Date: Thu, 25 Jun 2026 15:54:49 +0300 Subject: [PATCH 14/21] fixes --- docs/migration-SKILL.md | 65 ++++++----- docs/migration.md | 33 +++--- .../v1-to-v2/mappings/authSchemaNames.ts | 9 ++ .../v1-to-v2/mappings/schemaRouting.ts | 39 +++++++ .../migrations/v1-to-v2/mappings/symbolMap.ts | 7 ++ .../v1-to-v2/transforms/importPaths.ts | 67 ++++++------ .../v1-to-v2/transforms/mockPaths.ts | 101 +++++++++++------- .../test/v1-to-v2/authSchemaNames.test.ts | 11 +- .../v1-to-v2/transforms/importPaths.test.ts | 44 ++++++++ .../v1-to-v2/transforms/mockPaths.test.ts | 64 +++++++++++ .../v1-to-v2/transforms/symbolRenames.test.ts | 13 +++ 11 files changed, 340 insertions(+), 113 deletions(-) create mode 100644 packages/codemod/src/migrations/v1-to-v2/mappings/schemaRouting.ts diff --git a/docs/migration-SKILL.md b/docs/migration-SKILL.md index d13e4c69c1..cf5061733e 100644 --- a/docs/migration-SKILL.md +++ b/docs/migration-SKILL.md @@ -59,14 +59,14 @@ Replace all `@modelcontextprotocol/sdk/...` imports using this table. ### Types / shared imports -| v1 import path | v2 package | -| ------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `@modelcontextprotocol/sdk/types.js` | Types / error classes / enums / guards → `@modelcontextprotocol/client` or `@modelcontextprotocol/server`; Zod `*Schema` constants → `@modelcontextprotocol/sdk-shared` | -| `@modelcontextprotocol/sdk/shared/protocol.js` | `@modelcontextprotocol/client` or `@modelcontextprotocol/server` | -| `@modelcontextprotocol/sdk/shared/transport.js` | `@modelcontextprotocol/client` or `@modelcontextprotocol/server` | -| `@modelcontextprotocol/sdk/shared/uriTemplate.js` | `@modelcontextprotocol/client` or `@modelcontextprotocol/server` | +| v1 import path | v2 package | +| ------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `@modelcontextprotocol/sdk/types.js` | Types / error classes / enums / guards → `@modelcontextprotocol/client` or `@modelcontextprotocol/server`; Zod `*Schema` constants → `@modelcontextprotocol/sdk-shared` | +| `@modelcontextprotocol/sdk/shared/protocol.js` | `@modelcontextprotocol/client` or `@modelcontextprotocol/server` | +| `@modelcontextprotocol/sdk/shared/transport.js` | `@modelcontextprotocol/client` or `@modelcontextprotocol/server` | +| `@modelcontextprotocol/sdk/shared/uriTemplate.js` | `@modelcontextprotocol/client` or `@modelcontextprotocol/server` | | `@modelcontextprotocol/sdk/shared/auth.js` | Types / classes → `@modelcontextprotocol/client` or `@modelcontextprotocol/server`; OAuth/OpenID Zod `*Schema` constants (e.g. `OAuthTokensSchema`, `OAuthMetadataSchema`) → `@modelcontextprotocol/sdk-shared` | -| `@modelcontextprotocol/sdk/shared/stdio.js` | `@modelcontextprotocol/client` or `@modelcontextprotocol/server` (`ReadBuffer`, `serializeMessage`, `deserializeMessage` are in the root barrel; the `./stdio` subpath only has the transport class) | +| `@modelcontextprotocol/sdk/shared/stdio.js` | `@modelcontextprotocol/client` or `@modelcontextprotocol/server` (`ReadBuffer`, `serializeMessage`, `deserializeMessage` are in the root barrel; the `./stdio` subpath only has the transport class) | Notes: @@ -81,24 +81,27 @@ Notes: ## 5. Removed / Renamed Type Aliases and Symbols -| v1 (removed) | v2 (replacement) | -| ---------------------------------------- | --------------------------------------------------------------------------------------------------------------- | -| `JSONRPCError` | `JSONRPCErrorResponse` | -| `JSONRPCErrorSchema` | `JSONRPCErrorResponseSchema` | -| `isJSONRPCError` | `isJSONRPCErrorResponse` | -| `isJSONRPCResponse` (deprecated in v1) | `isJSONRPCResultResponse` (**not** v2's new `isJSONRPCResponse`, which correctly matches both result and error) | -| `ResourceReference` | `ResourceTemplateReference` | -| `ResourceReferenceSchema` | `ResourceTemplateReferenceSchema` | -| `IsomorphicHeaders` | REMOVED (use Web Standard `Headers`) | -| `AuthInfo` (from `server/auth/types.js`) | `AuthInfo` (now re-exported by `@modelcontextprotocol/client` and `@modelcontextprotocol/server`) | -| `McpError` | `ProtocolError` | -| `ErrorCode` | `ProtocolErrorCode` | -| `ErrorCode.RequestTimeout` | `SdkErrorCode.RequestTimeout` | -| `ErrorCode.ConnectionClosed` | `SdkErrorCode.ConnectionClosed` | -| `StreamableHTTPError` | REMOVED (use `SdkHttpError` with `SdkErrorCode.ClientHttp*`) | -| `WebSocketClientTransport` | REMOVED (use `StreamableHTTPClientTransport` or `StdioClientTransport`) | - -All other **type** symbols from `@modelcontextprotocol/sdk/types.js` retain their original names — import them from `@modelcontextprotocol/client` or `@modelcontextprotocol/server`. The **Zod schemas** (e.g., `CallToolResultSchema`, `ListToolsResultSchema`) move to `@modelcontextprotocol/sdk-shared`; `Schema.parse(value)` / `.safeParse(value)` keep working unchanged (the codemod rewrites the import path). To validate **without** depending on Zod, use `isSpecType.TypeName(value)` (e.g., `isSpecType.CallToolResult(v)`) or `specTypeSchemas.TypeName` (a `StandardSchemaV1Sync` validator) from `@modelcontextprotocol/client` / `@modelcontextprotocol/server`; the keys are typed as `SpecTypeName`, a literal union of all spec type names. +| v1 (removed) | v2 (replacement) | +| ------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `JSONRPCError` | `JSONRPCErrorResponse` | +| `JSONRPCErrorSchema` | `JSONRPCErrorResponseSchema` | +| `isJSONRPCError` | `isJSONRPCErrorResponse` | +| `isJSONRPCResponse` (deprecated in v1) | `isJSONRPCResultResponse` (**not** v2's new `isJSONRPCResponse`, which correctly matches both result and error) | +| `JSONRPCResponseSchema` (result-only in v1) | `JSONRPCResultResponseSchema` (from `@modelcontextprotocol/sdk-shared`; **not** v2's new `JSONRPCResponseSchema`, a `z.union` that also accepts error responses) | +| `ResourceReference` | `ResourceTemplateReference` | +| `ResourceReferenceSchema` | `ResourceTemplateReferenceSchema` | +| `IsomorphicHeaders` | REMOVED (use Web Standard `Headers`) | +| `AuthInfo` (from `server/auth/types.js`) | `AuthInfo` (now re-exported by `@modelcontextprotocol/client` and `@modelcontextprotocol/server`) | +| `McpError` | `ProtocolError` | +| `ErrorCode` | `ProtocolErrorCode` | +| `ErrorCode.RequestTimeout` | `SdkErrorCode.RequestTimeout` | +| `ErrorCode.ConnectionClosed` | `SdkErrorCode.ConnectionClosed` | +| `StreamableHTTPError` | REMOVED (use `SdkHttpError` with `SdkErrorCode.ClientHttp*`) | +| `WebSocketClientTransport` | REMOVED (use `StreamableHTTPClientTransport` or `StdioClientTransport`) | + +All other **type** symbols from `@modelcontextprotocol/sdk/types.js` retain their original names — import them from `@modelcontextprotocol/client` or `@modelcontextprotocol/server`. The **Zod schemas** (e.g., `CallToolResultSchema`, `ListToolsResultSchema`) move to +`@modelcontextprotocol/sdk-shared`; `Schema.parse(value)` / `.safeParse(value)` keep working unchanged (the codemod rewrites the import path). To validate **without** depending on Zod, use `isSpecType.TypeName(value)` (e.g., `isSpecType.CallToolResult(v)`) or +`specTypeSchemas.TypeName` (a `StandardSchemaV1Sync` validator) from `@modelcontextprotocol/client` / `@modelcontextprotocol/server`; the keys are typed as `SpecTypeName`, a literal union of all spec type names. ### Error class changes @@ -500,7 +503,8 @@ The 2025-11 task side-channel through `Protocol` is removed (was always `@experi `TaskStore` / `InMemoryTaskStore` / `CreateTaskOptions` / `isTerminal` (storage layer) are also removed; they will return with the SEP-2663 server-directed plugin. -NOT removed (wire surface, kept for 2025-11-25 interop): task Zod schemas + inferred types (`Task`, `TaskStatus`, `TaskMetadata`, `RelatedTaskMetadata`, `CreateTaskResult`, `GetTask*`, `ListTasks*`, `CancelTask*`, `TaskStatusNotification*`, `TaskAugmentedRequestParams`), task members of the request/result/notification unions, the `tasks` capability key, `isTaskAugmentedRequestParams`, `RELATED_TASK_META_KEY`. Inbound `tasks/*` requests → `-32601`. +NOT removed (wire surface, kept for 2025-11-25 interop): task Zod schemas + inferred types (`Task`, `TaskStatus`, `TaskMetadata`, `RelatedTaskMetadata`, `CreateTaskResult`, `GetTask*`, `ListTasks*`, `CancelTask*`, `TaskStatusNotification*`, `TaskAugmentedRequestParams`), task +members of the request/result/notification unions, the `tasks` capability key, `isTaskAugmentedRequestParams`, `RELATED_TASK_META_KEY`. Inbound `tasks/*` requests → `-32601`. ## 13. Behavioral Changes @@ -512,8 +516,10 @@ NOT removed (wire surface, kept for 2025-11-25 interop): task Zod schemas + infe No code changes required; these are wire-behavior notes: -- Resumability behavior (SSE priming events, `closeSSEStream` / `closeStandaloneSSEStream` callbacks) is only enabled for protocol versions in the transport's supported-versions list that are `>= 2025-11-25`. Unknown future version strings in an `initialize` request body no longer enable it. Behavior for all currently supported protocol versions is unchanged. -- Session-ID mismatch still responds `404 Not Found` with JSON-RPC error code `-32001` (`Session not found`), unchanged from v1. This `-32001` usage is an SDK convention, not a spec-assigned code, and may be re-derived as 2026 protocol revision error handling is adopted — migrated client code should key off the HTTP `404` status, not the `-32001` code. +- Resumability behavior (SSE priming events, `closeSSEStream` / `closeStandaloneSSEStream` callbacks) is only enabled for protocol versions in the transport's supported-versions list that are `>= 2025-11-25`. Unknown future version strings in an `initialize` request body no + longer enable it. Behavior for all currently supported protocol versions is unchanged. +- Session-ID mismatch still responds `404 Not Found` with JSON-RPC error code `-32001` (`Session not found`), unchanged from v1. This `-32001` usage is an SDK convention, not a spec-assigned code, and may be re-derived as 2026 protocol revision error handling is adopted — + migrated client code should key off the HTTP `404` status, not the `-32001` code. ## 14. Runtime-Specific JSON Schema Validators (Enhancement) @@ -557,5 +563,6 @@ Validator behavior: 8. If using server SSE transport, migrate to Streamable HTTP 9. If using server auth from the SDK: RS helpers (`requireBearerAuth`, `mcpAuthMetadataRouter`, `OAuthTokenVerifier`) → `@modelcontextprotocol/express`; AS helpers → `@modelcontextprotocol/server-legacy/auth` (deprecated); migrate AS to external IdP/OAuth library 10. If relying on `listTools()`/`listPrompts()`/etc. throwing on missing capabilities, set `enforceStrictCapabilities: true` -11. Format the changed files with the project's formatter (`prettier --write`, `eslint --fix`, or `biome format --write`) — edits are not reformatted automatically, and the wrapped schemas (step 5) and rewritten `setRequestHandler` method strings (section 9) frequently need it to satisfy lint +11. Format the changed files with the project's formatter (`prettier --write`, `eslint --fix`, or `biome format --write`) — edits are not reformatted automatically, and the wrapped schemas (step 5) and rewritten `setRequestHandler` method strings (section 9) frequently need it to + satisfy lint 12. Verify: build with `tsc` / run tests diff --git a/docs/migration.md b/docs/migration.md index ce78605439..468b1844ce 100644 --- a/docs/migration.md +++ b/docs/migration.md @@ -7,7 +7,8 @@ This guide covers the breaking changes introduced in v2 of the MCP TypeScript SD Version 2 of the MCP TypeScript SDK introduces several breaking changes to improve modularity, reduce dependency bloat, and provide a cleaner API surface. The biggest change is the split from a single `@modelcontextprotocol/sdk` package into separate `@modelcontextprotocol/core`, `@modelcontextprotocol/client`, and `@modelcontextprotocol/server` packages. -> **Formatting:** The `@modelcontextprotocol/codemod` package automates most of the mechanical changes below, but it rewrites your code's AST without reformatting it — wrapped schemas and generated handler method strings may not match your project's style. After migrating (with the codemod or by hand), run your formatter on the changed files — for example `prettier --write`, `eslint --fix`, or `biome format --write` — and review the diff. +> **Formatting:** The `@modelcontextprotocol/codemod` package automates most of the mechanical changes below, but it rewrites your code's AST without reformatting it — wrapped schemas and generated handler method strings may not match your project's style. After migrating (with +> the codemod or by hand), run your formatter on the changed files — for example `prettier --write`, `eslint --fix`, or `biome format --write` — and review the diff. ## Breaking Changes @@ -338,11 +339,11 @@ Note: the v2 signature takes a plain `string[]` instead of an options object. ### Resumability gating for unknown protocol versions (Streamable HTTP server) -The server-side Streamable HTTP transport enables resumability behavior introduced with protocol version `2025-11-25` — SSE priming events and the `closeSSEStream` / `closeStandaloneSSEStream` callbacks — based on the client's protocol version. Previously this was an -open-ended `protocolVersion >= '2025-11-25'` comparison, so an unrecognized future version string in an `initialize` request body (which, unlike the `MCP-Protocol-Version` header, is not validated against the supported-versions list) silently enabled the behavior. +The server-side Streamable HTTP transport enables resumability behavior introduced with protocol version `2025-11-25` — SSE priming events and the `closeSSEStream` / `closeStandaloneSSEStream` callbacks — based on the client's protocol version. Previously this was an open-ended +`protocolVersion >= '2025-11-25'` comparison, so an unrecognized future version string in an `initialize` request body (which, unlike the `MCP-Protocol-Version` header, is not validated against the supported-versions list) silently enabled the behavior. -The check is now bounded: the version must be one of the transport's supported protocol versions (after `connect()`, the server's `supportedProtocolVersions`) **and** at least `2025-11-25`. Behavior for all currently supported protocol versions (`2024-10-07` through -`2025-11-25`) is unchanged. Clients claiming an unknown future protocol version in the initialize body are now treated like clients without empty-SSE-data support: no priming event is sent and no early-close callbacks are provided. +The check is now bounded: the version must be one of the transport's supported protocol versions (after `connect()`, the server's `supportedProtocolVersions`) **and** at least `2025-11-25`. Behavior for all currently supported protocol versions (`2024-10-07` through `2025-11-25`) +is unchanged. Clients claiming an unknown future protocol version in the initialize body are now treated like clients without empty-SSE-data support: no priming event is sent and no early-close callbacks are provided. ### `setRequestHandler` and `setNotificationHandler` use method strings @@ -516,7 +517,8 @@ The return type is now inferred from the method name via `ResultTypeMap`. For ex For **custom (non-spec)** methods, keep the result-schema argument — see [Sending custom-method requests](#sending-custom-method-requests). Only drop the schema when calling a spec method. -If you were using `CallToolResultSchema` (or any `*Schema` constant) for **runtime validation** (not just in `request()`/`callTool()` calls), import the schema from `@modelcontextprotocol/sdk-shared`. Your `.parse()` / `.safeParse()` calls keep working unchanged — only the import path changes: +If you were using `CallToolResultSchema` (or any `*Schema` constant) for **runtime validation** (not just in `request()`/`callTool()` calls), import the schema from `@modelcontextprotocol/sdk-shared`. Your `.parse()` / `.safeParse()` calls keep working unchanged — only the import +path changes: ```typescript // v1 @@ -532,7 +534,8 @@ if (CallToolResultSchema.safeParse(value).success) { } ``` -`@modelcontextprotocol/sdk-shared` is the canonical home for the Zod schema constants — both the spec schemas and the OAuth/OpenID schemas (e.g. `OAuthTokensSchema`, `OAuthMetadataSchema`) that v1 exported from `@modelcontextprotocol/sdk/shared/auth.js`. `@modelcontextprotocol/server` and `@modelcontextprotocol/client` keep a Zod-free public surface (they export the corresponding TypeScript types, e.g. `OAuthTokens`), so the raw `*Schema` constants live in `sdk-shared`. (The codemod rewrites these imports for you.) +`@modelcontextprotocol/sdk-shared` is the canonical home for the Zod schema constants — both the spec schemas and the OAuth/OpenID schemas (e.g. `OAuthTokensSchema`, `OAuthMetadataSchema`) that v1 exported from `@modelcontextprotocol/sdk/shared/auth.js`. +`@modelcontextprotocol/server` and `@modelcontextprotocol/client` keep a Zod-free public surface (they export the corresponding TypeScript types, e.g. `OAuthTokens`), so the raw `*Schema` constants live in `sdk-shared`. (The codemod rewrites these imports for you.) If you'd rather **not** depend on Zod, `@modelcontextprotocol/client` and `@modelcontextprotocol/server` also expose Zod-free validators keyed by `SpecTypeName` — a literal union of every named spec type, so you get autocomplete and a compile error on typos: @@ -594,11 +597,15 @@ The following deprecated type aliases have been removed from `@modelcontextproto | `IsomorphicHeaders` | Use Web Standard `Headers` | | `AuthInfo` (from `server/auth/types.js`) | `AuthInfo` (now re-exported by `@modelcontextprotocol/client` and `@modelcontextprotocol/server`) | -All other symbols exported from `@modelcontextprotocol/sdk/types.js` retain their original names. Import the **types**, error classes, enums, and guards from `@modelcontextprotocol/client` or `@modelcontextprotocol/server`, and the **Zod schemas** (the `*Schema` constants) from `@modelcontextprotocol/sdk-shared`. +All other symbols exported from `@modelcontextprotocol/sdk/types.js` retain their original names. Import the **types**, error classes, enums, and guards from `@modelcontextprotocol/client` or `@modelcontextprotocol/server`, and the **Zod schemas** (the `*Schema` constants) from +`@modelcontextprotocol/sdk-shared`. > **Note on `isJSONRPCResponse`:** v1's `isJSONRPCResponse` was a deprecated alias that only checked for _result_ responses (it was equivalent to `isJSONRPCResultResponse`). v2 removes the deprecated alias and introduces a **new** `isJSONRPCResponse` with corrected semantics — it > checks for _any_ response (either result or error). If you are migrating v1 code that used `isJSONRPCResponse`, rename it to `isJSONRPCResultResponse` to preserve the original behavior. Use the new `isJSONRPCResponse` only when you want to match both result and error responses. +> **Note on `JSONRPCResponseSchema`:** the Zod schema follows the same pattern. v1's `JSONRPCResponseSchema` validated only _result_ responses; v2 reuses the name for a `z.union([JSONRPCResultResponseSchema, JSONRPCErrorResponseSchema])` that also accepts error responses. If you +> are migrating v1 code that called `JSONRPCResponseSchema.parse()`/`.safeParse()`, rename it to `JSONRPCResultResponseSchema` (re-exported by `@modelcontextprotocol/sdk-shared`) to preserve the original validation. The codemod performs this rename automatically. + **Before (v1):** ```typescript @@ -914,7 +921,9 @@ The 2025-11 experimental tasks side-channel woven through `Protocol` has been re **Also removed:** the storage layer (`TaskStore`, `InMemoryTaskStore`, `CreateTaskOptions`, `isTerminal`). It will return as part of the SEP-2663 server-directed plugin in a follow-up. -**Wire types remain.** The task wire surface defined by the 2025-11-25 protocol revision is still exported, for interoperability with peers on that revision: the task Zod schemas and their inferred types (`Task`, `TaskStatus`, `TaskMetadata`, `RelatedTaskMetadata`, `CreateTaskResult`, `GetTask*`, `GetTaskPayload*`, `ListTasks*`, `CancelTask*`, `TaskStatusNotification*`, `TaskAugmentedRequestParams`), the task members of the request/result/notification unions, the `tasks` capability key, the `isTaskAugmentedRequestParams` guard, and `RELATED_TASK_META_KEY`. Only the behavior is gone: servers built on this SDK do not advertise the `tasks` capability, and inbound `tasks/*` requests receive a standard `-32601` (method not found) error. +**Wire types remain.** The task wire surface defined by the 2025-11-25 protocol revision is still exported, for interoperability with peers on that revision: the task Zod schemas and their inferred types (`Task`, `TaskStatus`, `TaskMetadata`, `RelatedTaskMetadata`, +`CreateTaskResult`, `GetTask*`, `GetTaskPayload*`, `ListTasks*`, `CancelTask*`, `TaskStatusNotification*`, `TaskAugmentedRequestParams`), the task members of the request/result/notification unions, the `tasks` capability key, the `isTaskAugmentedRequestParams` guard, and +`RELATED_TASK_META_KEY`. Only the behavior is gone: servers built on this SDK do not advertise the `tasks` capability, and inbound `tasks/*` requests receive a standard `-32601` (method not found) error. There is no migration path for the removed surface; it was always `@experimental`. Task support is planned to return as an opt-in extension plugin per SEP-2663. @@ -1009,9 +1018,9 @@ The following APIs are unchanged between v1 and v2 (only the import paths change - All Zod schemas and type definitions from `types.ts` (except the aliases listed above) - Tool, prompt, and resource callback return types -**Session-ID mismatch responses**: when session management is enabled and a request carries an `Mcp-Session-Id` header that doesn't match the active session, the Streamable HTTP server transport responds `404 Not Found` with a JSON-RPC error body using code `-32001` and -message `Session not found` — unchanged from v1. Note that this use of `-32001` is an SDK convention, not a spec-assigned error code, and it is expected to be re-derived as error handling for the 2026 protocol revision (`2026-07-28`) is adopted. Avoid hard-coding the -`-32001` code in client logic; key off the HTTP `404` status instead. +**Session-ID mismatch responses**: when session management is enabled and a request carries an `Mcp-Session-Id` header that doesn't match the active session, the Streamable HTTP server transport responds `404 Not Found` with a JSON-RPC error body using code `-32001` and message +`Session not found` — unchanged from v1. Note that this use of `-32001` is an SDK convention, not a spec-assigned error code, and it is expected to be re-derived as error handling for the 2026 protocol revision (`2026-07-28`) is adopted. Avoid hard-coding the `-32001` code in +client logic; key off the HTTP `404` status instead. ## Using an LLM to migrate your code diff --git a/packages/codemod/src/migrations/v1-to-v2/mappings/authSchemaNames.ts b/packages/codemod/src/migrations/v1-to-v2/mappings/authSchemaNames.ts index d643e87593..869d507bbb 100644 --- a/packages/codemod/src/migrations/v1-to-v2/mappings/authSchemaNames.ts +++ b/packages/codemod/src/migrations/v1-to-v2/mappings/authSchemaNames.ts @@ -19,3 +19,12 @@ export const AUTH_SCHEMA_NAMES: ReadonlySet = new Set([ 'OpenIdProviderDiscoveryMetadataSchema', 'OpenIdProviderMetadataSchema' ]); + +// v1's `@modelcontextprotocol/sdk/shared/auth.js` also exported these as Zod schema CONSTANTS, but they +// are typeless internal URL field-validators (no public spec type), so v2's sdk-shared deliberately does +// NOT re-export them (see packages/sdk-shared/src/index.ts) and no other public v2 package exports them. +// They therefore have no v2 home: routed by context they would produce a codemod-introduced "has no +// exported member" error. importPaths emits an actionRequired diagnostic instead of silently breaking. +// test/v1-to-v2/authSchemaNames.test.ts asserts these are NOT in AUTH_SCHEMA_NAMES (and so are not +// claimed to be sdk-shared exports). +export const AUTH_SCHEMA_NAMES_NO_V2_PUBLIC_EXPORT: ReadonlySet = new Set(['SafeUrlSchema', 'OptionalSafeUrlSchema']); diff --git a/packages/codemod/src/migrations/v1-to-v2/mappings/schemaRouting.ts b/packages/codemod/src/migrations/v1-to-v2/mappings/schemaRouting.ts new file mode 100644 index 0000000000..d5b18a7431 --- /dev/null +++ b/packages/codemod/src/migrations/v1-to-v2/mappings/schemaRouting.ts @@ -0,0 +1,39 @@ +// Shared per-symbol routing logic for v1→v2 import/export/mock rewrites. Centralized here so the +// import-path transform (static imports/re-exports) and the mock-path transform (vi.mock/jest.mock +// factories, dynamic import() destructurings) route a given symbol to exactly the same v2 package. +import { AUTH_SCHEMA_NAMES } from './authSchemaNames'; +import type { ImportMapping } from './importMap'; +import { SPEC_SCHEMA_NAMES } from './specSchemaNames'; +import { SIMPLE_RENAMES } from './symbolMap'; + +/** The v2 name a symbol resolves to after renames (per-mapping override, then global SIMPLE_RENAMES). */ +export function resolveRenamedName(name: string, mapping: ImportMapping): string { + return mapping.renamedSymbols?.[name] ?? SIMPLE_RENAMES[name] ?? name; +} + +/** + * True when `name` (after renames) is a Zod schema CONSTANT that sdk-shared re-exports — either a spec + * schema (`SPEC_SCHEMA_NAMES`) or an OAuth/OpenID schema (`AUTH_SCHEMA_NAMES`). Membership (not a + * `*Schema` suffix) is what keeps TYPES whose name ends in `Schema` — e.g. `BooleanSchema` — out. + */ +export function isSharedSchemaConst(name: string, mapping: ImportMapping): boolean { + const resolved = resolveRenamedName(name, mapping); + return SPEC_SCHEMA_NAMES.has(resolved) || AUTH_SCHEMA_NAMES.has(resolved); +} + +/** + * The per-symbol target package for a symbol imported/re-exported/mocked from `mapping`'s module, or + * `undefined` when the symbol should use the mapping's resolved `target`. Exact-name + * `symbolTargetOverrides` win over `schemaSymbolTarget`, which routes a symbol to the shared-schemas + * package only when its rename-resolved name is a schema constant re-exported by sdk-shared (see + * `isSharedSchemaConst`). + */ +export function symbolTargetOverride(name: string, mapping: ImportMapping): string | undefined { + if (mapping.symbolTargetOverrides && name in mapping.symbolTargetOverrides) { + return mapping.symbolTargetOverrides[name]; + } + if (mapping.schemaSymbolTarget && isSharedSchemaConst(name, mapping)) { + return mapping.schemaSymbolTarget; + } + return undefined; +} diff --git a/packages/codemod/src/migrations/v1-to-v2/mappings/symbolMap.ts b/packages/codemod/src/migrations/v1-to-v2/mappings/symbolMap.ts index 7671a3ee0d..f397679913 100644 --- a/packages/codemod/src/migrations/v1-to-v2/mappings/symbolMap.ts +++ b/packages/codemod/src/migrations/v1-to-v2/mappings/symbolMap.ts @@ -4,6 +4,13 @@ export const SIMPLE_RENAMES: Record = { JSONRPCErrorSchema: 'JSONRPCErrorResponseSchema', isJSONRPCError: 'isJSONRPCErrorResponse', isJSONRPCResponse: 'isJSONRPCResultResponse', + // v1's JSONRPCResponseSchema validated only *result* responses. v2 reuses the name for a + // z.union([JSONRPCResultResponseSchema, JSONRPCErrorResponseSchema]) that also accepts error + // responses, so a migrated `JSONRPCResponseSchema.parse(...)` would silently widen. Rename to the + // result-only schema to preserve v1 behavior — mirroring the isJSONRPCResponse guard rename above. + // (The TYPE JSONRPCResponse/JSONRPCResultResponse is not part of the public v2 surface, so only the + // schema constant — re-exported by sdk-shared — is renamed here.) + JSONRPCResponseSchema: 'JSONRPCResultResponseSchema', ResourceReference: 'ResourceTemplateReference', ResourceReferenceSchema: 'ResourceTemplateReferenceSchema' }; diff --git a/packages/codemod/src/migrations/v1-to-v2/transforms/importPaths.ts b/packages/codemod/src/migrations/v1-to-v2/transforms/importPaths.ts index f6824e3945..ba89263645 100644 --- a/packages/codemod/src/migrations/v1-to-v2/transforms/importPaths.ts +++ b/packages/codemod/src/migrations/v1-to-v2/transforms/importPaths.ts @@ -7,10 +7,9 @@ import { actionRequired, info, v2Gap, warning } from '../../../utils/diagnostics import type { NamedImportSpec } from '../../../utils/importUtils'; import { addOrMergeImport, getSdkExports, getSdkImports, isTypeOnlyImport } from '../../../utils/importUtils'; import { resolveTypesPackage } from '../../../utils/projectAnalyzer'; -import { AUTH_SCHEMA_NAMES } from '../mappings/authSchemaNames'; -import type { ImportMapping } from '../mappings/importMap'; +import { AUTH_SCHEMA_NAMES_NO_V2_PUBLIC_EXPORT } from '../mappings/authSchemaNames'; import { isAuthImport, lookupImportMapping } from '../mappings/importMap'; -import { SPEC_SCHEMA_NAMES } from '../mappings/specSchemaNames'; +import { isSharedSchemaConst, resolveRenamedName, symbolTargetOverride } from '../mappings/schemaRouting'; import { SIMPLE_RENAMES } from '../mappings/symbolMap'; const REEXPORT_WARNINGS: Record = { @@ -22,38 +21,6 @@ const REEXPORT_WARNINGS: Record = { 'Re-exported StreamableHTTPError was renamed to SdkHttpError in v2 with a different constructor. Update this re-export manually.' }; -/** The v2 name a symbol resolves to after renames (per-mapping override, then global SIMPLE_RENAMES). */ -function resolveRenamedName(name: string, mapping: ImportMapping): string { - return mapping.renamedSymbols?.[name] ?? SIMPLE_RENAMES[name] ?? name; -} - -/** - * True when `name` (after renames) is a Zod schema CONSTANT that sdk-shared re-exports — either a spec - * schema (`SPEC_SCHEMA_NAMES`) or an OAuth/OpenID schema (`AUTH_SCHEMA_NAMES`). Membership (not a - * `*Schema` suffix) is what keeps TYPES whose name ends in `Schema` — e.g. `BooleanSchema` — out. - */ -function isSharedSchemaConst(name: string, mapping: ImportMapping): boolean { - const resolved = resolveRenamedName(name, mapping); - return SPEC_SCHEMA_NAMES.has(resolved) || AUTH_SCHEMA_NAMES.has(resolved); -} - -/** - * The per-symbol target package for a symbol imported/re-exported from `mapping`'s module, or - * `undefined` when the symbol should use the mapping's resolved `target`. Exact-name - * `symbolTargetOverrides` win over `schemaSymbolTarget`, which routes a symbol to the shared-schemas - * package only when its rename-resolved name is a schema constant re-exported by sdk-shared (see - * `isSharedSchemaConst`). - */ -function symbolTargetOverride(name: string, mapping: ImportMapping): string | undefined { - if (mapping.symbolTargetOverrides && name in mapping.symbolTargetOverrides) { - return mapping.symbolTargetOverrides[name]; - } - if (mapping.schemaSymbolTarget && isSharedSchemaConst(name, mapping)) { - return mapping.schemaSymbolTarget; - } - return undefined; -} - export const importPathsTransform: Transform = { name: 'Import path rewrites', id: 'imports', @@ -240,6 +207,20 @@ export const importPathsTransform: Transform = { const resolvedName = mapping.renamedSymbols?.[name] ?? name; const specifierTypeOnly = typeOnly || n.isTypeOnly(); const symbolTarget = symbolTargetOverride(name, mapping) ?? targetPackage; + // A v1 auth-schema constant with no public v2 home (SafeUrlSchema/OptionalSafeUrlSchema) + // routes by context to a package that doesn't export it. Flag it so the user inlines the + // validation instead of hitting a silent "has no exported member" error. + if (mapping.schemaSymbolTarget && AUTH_SCHEMA_NAMES_NO_V2_PUBLIC_EXPORT.has(name)) { + diagnostics.push( + actionRequired( + filePath, + imp, + `${name} was an internal URL field-validator in v1's ${specifier} with no public v2 equivalent ` + + `(it is not re-exported by @modelcontextprotocol/sdk-shared). Remove this import and inline the ` + + `validation (e.g. validate the URL with the WHATWG \`URL\` constructor or your own Zod schema).` + ) + ); + } usedPackages.add(symbolTarget); addPending(symbolTarget, [alias ? { name: resolvedName, alias } : resolvedName], specifierTypeOnly); } @@ -353,6 +334,22 @@ function rewriteExportDeclarations( if (mapping.symbolTargetOverrides || mapping.schemaSymbolTarget) { const namedExports = exp.getNamedExports(); + // A star re-export (`export * from …`, including `export * as ns from …`) has no named + // exports to route per-symbol, so it moves wholesale to the context package — which exports + // none of the Zod `*Schema` constants the v1 module re-exported. Downstream consumers of this + // barrel would hit "has no exported member" with no pointer to where the schemas went, so flag + // it (mirroring the namespace-import diagnostic on the import side). + if (mapping.schemaSymbolTarget && namedExports.length === 0) { + diagnostics.push( + actionRequired( + filePath, + exp, + `Star re-export of ${specifier} will not include the Zod schema constants that moved to ` + + `${mapping.schemaSymbolTarget} (they are no longer exported by ${targetPackage}). ` + + `Add an explicit \`export { … } from '${mapping.schemaSymbolTarget}'\` for any re-exported \`*Schema\` constants.` + ) + ); + } const overrides = namedExports.map(s => symbolTargetOverride(s.getName(), mapping)); const uniqueOverrides = new Set(overrides.filter((t): t is string => t !== undefined)); const allOverridden = namedExports.length > 0 && overrides.every(t => t !== undefined); diff --git a/packages/codemod/src/migrations/v1-to-v2/transforms/mockPaths.ts b/packages/codemod/src/migrations/v1-to-v2/transforms/mockPaths.ts index 416d75129c..c212d70640 100644 --- a/packages/codemod/src/migrations/v1-to-v2/transforms/mockPaths.ts +++ b/packages/codemod/src/migrations/v1-to-v2/transforms/mockPaths.ts @@ -5,9 +5,29 @@ import type { Diagnostic, Transform, TransformContext, TransformResult } from '. import { actionRequired, v2Gap, warning } from '../../../utils/diagnostics'; import { isSdkSpecifier } from '../../../utils/importUtils'; import { resolveTypesPackage } from '../../../utils/projectAnalyzer'; +import type { ImportMapping } from '../mappings/importMap'; import { isAuthImport, lookupImportMapping } from '../mappings/importMap'; +import { symbolTargetOverride } from '../mappings/schemaRouting'; import { SIMPLE_RENAMES } from '../mappings/symbolMap'; +/** + * Resolve the single per-symbol target package shared by every `symbol` (mocked factory keys or + * destructured `import()` bindings), or report that they mix v2 packages. A mock/dynamic-import + * specifier is a single string and cannot be split, so a mix can only be flagged, not rewritten. + * Returns `target: undefined` when no symbol carries a per-symbol override (the caller keeps the + * mapping's resolved context/`target` package). Mirrors `symbolTargetOverride` routing used by the + * static import/export transform so e.g. a factory of only `*Schema` constants routes to sdk-shared. + */ +function routeSymbols(symbols: string[], mapping: ImportMapping): { target?: string; mixed: boolean } { + if (symbols.length === 0) return { mixed: false }; + const targets = symbols.map(s => symbolTargetOverride(s, mapping)); + const overridden = targets.filter((t): t is string => t !== undefined); + const unique = new Set(overridden); + if (overridden.length === symbols.length && unique.size === 1) return { target: [...unique][0]!, mixed: false }; + if (unique.size > 0) return { mixed: true }; + return { mixed: false }; +} + const MOCK_METHODS = new Set([ 'mock', 'doMock', @@ -54,12 +74,12 @@ function resolveTarget( context: TransformContext, sourceFile: SourceFile, diagnosticSink?: { filePath: string; line: number; diagnostics: Diagnostic[] } -): - | { target: string; renamedSymbols?: Record; symbolTargetOverrides?: Record } - | { removed: true; isV2Gap?: boolean; removalMessage?: string } - | null { +): { target: string; mapping: ImportMapping } | { removed: true; isV2Gap?: boolean; removalMessage?: string } | null { const mapping = lookupImportMapping(specifier); - if (!mapping && isAuthImport(specifier)) return { target: '@modelcontextprotocol/server-legacy/auth' }; + if (!mapping && isAuthImport(specifier)) { + const authMapping: ImportMapping = { target: '@modelcontextprotocol/server-legacy/auth', status: 'moved' }; + return { target: authMapping.target, mapping: authMapping }; + } if (!mapping) return null; if (mapping.status === 'removed') return { removed: true, isV2Gap: mapping.isV2Gap, removalMessage: mapping.removalMessage }; @@ -79,7 +99,10 @@ function resolveTarget( } } - return { target, renamedSymbols: mapping.renamedSymbols, symbolTargetOverrides: mapping.symbolTargetOverrides }; + // Return the original mapping (not just `renamedSymbols`/`symbolTargetOverrides`) so per-symbol + // routing can consult `schemaSymbolTarget` via the shared `symbolTargetOverride`/`routeSymbols`, + // matching how the static import transform routes `*Schema` constants to sdk-shared. + return { target, mapping }; } function rewriteMockCall( @@ -122,13 +145,15 @@ function rewriteMockCall( let changes = 0; let effectiveTarget = resolved.target; - if (resolved.symbolTargetOverrides && args.length >= 2) { - const factorySymbols = collectFactorySymbols(args[1]!); - const allOverridden = factorySymbols.length > 0 && factorySymbols.every(s => s in resolved.symbolTargetOverrides!); - const someOverridden = factorySymbols.some(s => s in resolved.symbolTargetOverrides!); - if (allOverridden) { - effectiveTarget = resolved.symbolTargetOverrides[factorySymbols[0]!]!; - } else if (someOverridden) { + if (args.length >= 2) { + // Route the factory's mocked symbols the same way the static import transform would: a factory of + // only `*Schema` constants (from sdk/types.js or sdk/shared/auth.js) moves to sdk-shared; a factory + // of only `StreamableHTTPServerTransport` moves to @modelcontextprotocol/node. A single mock path + // can't be split, so a mix of packages is flagged for manual migration. + const { target: routedTarget, mixed } = routeSymbols(collectFactorySymbols(args[1]!), resolved.mapping); + if (routedTarget) { + effectiveTarget = routedTarget; + } else if (mixed) { diagnostics.push( actionRequired( sourceFile.getFilePath(), @@ -144,7 +169,7 @@ function rewriteMockCall( firstArg.setLiteralValue(effectiveTarget); changes++; - const allRenames: Record = { ...SIMPLE_RENAMES, ...resolved.renamedSymbols }; + const allRenames: Record = { ...SIMPLE_RENAMES, ...resolved.mapping.renamedSymbols }; if (args.length >= 2) { changes += renameSymbolsInFactory(args[1]!, allRenames); } @@ -263,27 +288,31 @@ function rewriteDynamicImports( } let effectiveTarget = resolved.target; - const allRenames: Record = { ...SIMPLE_RENAMES, ...resolved.renamedSymbols }; - - // Check if destructured symbols should route to an override target - if (resolved.symbolTargetOverrides) { - const parent = node.getParent(); - if (parent && Node.isAwaitExpression(parent)) { - const grandParent = parent.getParent(); - if (grandParent && Node.isVariableDeclaration(grandParent)) { - const nameNode = grandParent.getNameNode(); - if (Node.isObjectBindingPattern(nameNode)) { - const elements = nameNode.getElements(); - const allOverridden = - elements.length > 0 && - elements.every(el => { - const key = el.getPropertyNameNode()?.getText() ?? el.getName(); - return key in resolved.symbolTargetOverrides!; - }); - if (allOverridden) { - effectiveTarget = - resolved.symbolTargetOverrides[elements[0]!.getPropertyNameNode()?.getText() ?? elements[0]!.getName()]!; - } + const allRenames: Record = { ...SIMPLE_RENAMES, ...resolved.mapping.renamedSymbols }; + + // Route the destructured bindings the same way the static import transform would: a destructuring + // of only `*Schema` constants (e.g. `const { CallToolResultSchema } = await import('…/types.js')`) + // moves to sdk-shared, and `StreamableHTTPServerTransport` moves to @modelcontextprotocol/node. A + // single import() specifier can't be split, so a mix of packages is flagged for manual migration. + const parentExpr = node.getParent(); + if (parentExpr && Node.isAwaitExpression(parentExpr)) { + const grandParent = parentExpr.getParent(); + if (grandParent && Node.isVariableDeclaration(grandParent)) { + const nameNode = grandParent.getNameNode(); + if (Node.isObjectBindingPattern(nameNode)) { + const keys = nameNode.getElements().map(el => el.getPropertyNameNode()?.getText() ?? el.getName()); + const { target: routedTarget, mixed } = routeSymbols(keys, resolved.mapping); + if (routedTarget) { + effectiveTarget = routedTarget; + } else if (mixed) { + diagnostics.push( + actionRequired( + sourceFile.getFilePath(), + node, + `Dynamic import of ${specifier} destructures symbols that belong to different v2 packages. ` + + `Split the import manually so each symbol targets the correct package.` + ) + ); } } } @@ -314,7 +343,7 @@ function rewriteDynamicImports( } } } - const moduleRenames = resolved.renamedSymbols ?? {}; + const moduleRenames = resolved.mapping.renamedSymbols ?? {}; if (!Node.isObjectBindingPattern(nameNode) && Object.keys(moduleRenames).length > 0) { diagnostics.push( actionRequired( diff --git a/packages/codemod/test/v1-to-v2/authSchemaNames.test.ts b/packages/codemod/test/v1-to-v2/authSchemaNames.test.ts index cb8f0f718c..71ab402667 100644 --- a/packages/codemod/test/v1-to-v2/authSchemaNames.test.ts +++ b/packages/codemod/test/v1-to-v2/authSchemaNames.test.ts @@ -3,7 +3,7 @@ import { fileURLToPath } from 'node:url'; import { describe, expect, it } from 'vitest'; -import { AUTH_SCHEMA_NAMES } from '../../src/migrations/v1-to-v2/mappings/authSchemaNames.js'; +import { AUTH_SCHEMA_NAMES, AUTH_SCHEMA_NAMES_NO_V2_PUBLIC_EXPORT } from '../../src/migrations/v1-to-v2/mappings/authSchemaNames.js'; describe('AUTH_SCHEMA_NAMES (codemod auth schema-routing allowlist)', () => { it('routes only auth schemas that @modelcontextprotocol/sdk-shared exports (drift guard)', () => { @@ -24,4 +24,13 @@ describe('AUTH_SCHEMA_NAMES (codemod auth schema-routing allowlist)', () => { // The v1 auth-schema set is frozen; pin its size so an accidental add/remove is caught. expect(AUTH_SCHEMA_NAMES.size).toBe(11); }); + + it('keeps the no-v2-home auth schemas OUT of the routing allowlist', () => { + // SafeUrlSchema/OptionalSafeUrlSchema have no public v2 export, so they must NOT be routed to + // sdk-shared (the import transform flags them instead). Guard the two sets stay disjoint. + for (const name of AUTH_SCHEMA_NAMES_NO_V2_PUBLIC_EXPORT) { + expect(AUTH_SCHEMA_NAMES.has(name)).toBe(false); + } + expect([...AUTH_SCHEMA_NAMES_NO_V2_PUBLIC_EXPORT].sort()).toEqual(['OptionalSafeUrlSchema', 'SafeUrlSchema']); + }); }); diff --git a/packages/codemod/test/v1-to-v2/transforms/importPaths.test.ts b/packages/codemod/test/v1-to-v2/transforms/importPaths.test.ts index 6e46b1fd3c..de00e2941a 100644 --- a/packages/codemod/test/v1-to-v2/transforms/importPaths.test.ts +++ b/packages/codemod/test/v1-to-v2/transforms/importPaths.test.ts @@ -323,6 +323,50 @@ describe('import-paths transform', () => { expect(result).not.toContain('@modelcontextprotocol/sdk/types'); }); + it('routes JSONRPCResponseSchema (result-only in v1) from sdk/types.js to sdk-shared', () => { + // v1's JSONRPCResponseSchema validated only result responses; v2 reuses the name for a union. + // The rename to JSONRPCResultResponseSchema (a sdk-shared export) preserves v1 behavior; importPaths + // routes it to sdk-shared against the rename-resolved name (symbolRenames applies the rename after). + const input = `import { JSONRPCResponseSchema } from '@modelcontextprotocol/sdk/types.js';\n`; + const result = applyTransform(input, { projectType: 'server' }); + expect(result).toContain(`from "@modelcontextprotocol/sdk-shared"`); + expect(result).not.toContain('@modelcontextprotocol/sdk/types'); + expect(result).not.toContain(`from "@modelcontextprotocol/server"`); + }); + + it('flags a SafeUrlSchema import from sdk/shared/auth.js (no public v2 equivalent)', () => { + // SafeUrlSchema/OptionalSafeUrlSchema were internal URL field-validators in v1; v2's sdk-shared + // deliberately does not re-export them, so there is no v2 home — emit guidance instead of silently + // routing to a package that has no such export. + const input = `import { SafeUrlSchema, OptionalSafeUrlSchema } from '@modelcontextprotocol/sdk/shared/auth.js';\n`; + const project = new Project({ useInMemoryFileSystem: true }); + const sourceFile = project.createSourceFile('test.ts', input); + const result = importPathsTransform.apply(sourceFile, { projectType: 'server' }); + const messages = result.diagnostics.map(d => d.message); + expect(messages.some(m => m.includes('SafeUrlSchema') && m.includes('no public v2 equivalent'))).toBe(true); + expect(messages.some(m => m.includes('OptionalSafeUrlSchema') && m.includes('no public v2 equivalent'))).toBe(true); + }); + + it('flags a star re-export of sdk/types.js that drops the moved schema constants', () => { + // `export * from '…/types.js'` cannot be routed per-symbol, so the Zod *Schema constants (now in + // sdk-shared) silently disappear from the re-exporting barrel. Surface that for the user. + const input = `export * from '@modelcontextprotocol/sdk/types.js';\n`; + const project = new Project({ useInMemoryFileSystem: true }); + const sourceFile = project.createSourceFile('test.ts', input); + const result = importPathsTransform.apply(sourceFile, { projectType: 'server' }); + const messages = result.diagnostics.map(d => d.message).join('\n'); + expect(messages).toContain('@modelcontextprotocol/sdk-shared'); + expect(messages).toMatch(/Star re-export/i); + }); + + it('flags a star re-export of sdk/shared/auth.js (schema constants move to sdk-shared)', () => { + const input = `export * from '@modelcontextprotocol/sdk/shared/auth.js';\n`; + const project = new Project({ useInMemoryFileSystem: true }); + const sourceFile = project.createSourceFile('test.ts', input); + const result = importPathsTransform.apply(sourceFile, { projectType: 'server' }); + expect(result.diagnostics.map(d => d.message).join('\n')).toContain('@modelcontextprotocol/sdk-shared'); + }); + it('emits a split diagnostic for a re-export mixing a spec schema and a *Schema type (no silent breakage)', () => { // The `*Schema` suffix would have routed BooleanSchema to sdk-shared silently (no such export); // membership routing instead surfaces the mismatch so the user splits the re-export manually. diff --git a/packages/codemod/test/v1-to-v2/transforms/mockPaths.test.ts b/packages/codemod/test/v1-to-v2/transforms/mockPaths.test.ts index 03c75e204d..4dd8d93bbf 100644 --- a/packages/codemod/test/v1-to-v2/transforms/mockPaths.test.ts +++ b/packages/codemod/test/v1-to-v2/transforms/mockPaths.test.ts @@ -338,6 +338,70 @@ describe('mock-paths transform', () => { }); }); + describe('schema constant routing (schemaSymbolTarget)', () => { + it('routes a vi.mock factory of only spec *Schema constants to sdk-shared', () => { + const input = [`vi.mock('@modelcontextprotocol/sdk/types.js', () => ({`, ` CallToolResultSchema: vi.fn()`, `}));`, ''].join( + '\n' + ); + const result = applyTransform(input); + expect(result).toContain(`'@modelcontextprotocol/sdk-shared'`); + expect(result).not.toContain('@modelcontextprotocol/sdk/types'); + // The schema constant lives in sdk-shared, never the context (server) package. + expect(result).not.toContain(`'@modelcontextprotocol/server'`); + }); + + it('routes a vi.mock factory of only auth *Schema constants to sdk-shared', () => { + const input = [ + `vi.mock('@modelcontextprotocol/sdk/shared/auth.js', () => ({`, + ` OAuthTokensSchema: vi.fn()`, + `}));`, + '' + ].join('\n'); + const result = applyTransform(input); + expect(result).toContain(`'@modelcontextprotocol/sdk-shared'`); + expect(result).not.toContain('@modelcontextprotocol/sdk/shared/auth'); + }); + + it('routes a destructured dynamic import of only *Schema constants to sdk-shared', () => { + const input = [`const { CallToolResultSchema } = await import('@modelcontextprotocol/sdk/types.js');`, ''].join('\n'); + const result = applyTransform(input); + expect(result).toContain(`import('@modelcontextprotocol/sdk-shared')`); + expect(result).not.toContain('@modelcontextprotocol/sdk/types'); + }); + + it('renames JSONRPCResponseSchema and routes it to sdk-shared in a mock factory', () => { + const input = [`vi.mock('@modelcontextprotocol/sdk/types.js', () => ({`, ` JSONRPCResponseSchema: vi.fn()`, `}));`, ''].join( + '\n' + ); + const result = applyTransform(input); + expect(result).toContain(`'@modelcontextprotocol/sdk-shared'`); + expect(result).toContain('JSONRPCResultResponseSchema'); + expect(result).not.toMatch(/(? { + const input = [ + `vi.mock('@modelcontextprotocol/sdk/types.js', () => ({`, + ` CallToolResultSchema: vi.fn(),`, + ` McpError: vi.fn()`, + `}));`, + '' + ].join('\n'); + const project = new Project({ useInMemoryFileSystem: true }); + const sourceFile = project.createSourceFile('test.ts', input); + const result = mockPathsTransform.apply(sourceFile, ctx); + expect(result.diagnostics.some(d => d.message.includes('mixes symbols that belong to different v2 packages'))).toBe(true); + }); + + it('flags a destructured dynamic import mixing a *Schema constant and a type', () => { + const input = [`const { CallToolResultSchema, McpError } = await import('@modelcontextprotocol/sdk/types.js');`, ''].join('\n'); + const project = new Project({ useInMemoryFileSystem: true }); + const sourceFile = project.createSourceFile('test.ts', input); + const result = mockPathsTransform.apply(sourceFile, ctx); + expect(result.diagnostics.some(d => d.message.includes('belong to different v2 packages'))).toBe(true); + }); + }); + describe('validator subpath rewrites', () => { it('rewrites vi.mock of validator provider to the subpath', () => { const input = [ diff --git a/packages/codemod/test/v1-to-v2/transforms/symbolRenames.test.ts b/packages/codemod/test/v1-to-v2/transforms/symbolRenames.test.ts index 9a227d2f4d..9e3b1582ef 100644 --- a/packages/codemod/test/v1-to-v2/transforms/symbolRenames.test.ts +++ b/packages/codemod/test/v1-to-v2/transforms/symbolRenames.test.ts @@ -44,6 +44,19 @@ describe('symbol-renames transform', () => { expect(result).toContain('isJSONRPCResultResponse'); }); + it('renames JSONRPCResponseSchema to JSONRPCResultResponseSchema (result-only in v1)', () => { + // v1's JSONRPCResponseSchema validated only result responses; v2 reuses the name for a union that + // also accepts errors. Rename to the result-only schema to preserve v1 parse/safeParse behavior. + const input = [ + `import { JSONRPCResponseSchema } from '@modelcontextprotocol/sdk/types.js';`, + `const r = JSONRPCResponseSchema.parse(value);`, + '' + ].join('\n'); + const result = applyTransform(input); + expect(result).toContain('JSONRPCResultResponseSchema.parse(value)'); + expect(result).not.toMatch(/(? { const input = [ `import { ResourceReference } from '@modelcontextprotocol/sdk/types.js';`, From b452a80cb985f287f608ba989a711396bb3e451f Mon Sep 17 00:00:00 2001 From: Konstantin Konstantinov Date: Thu, 25 Jun 2026 17:02:26 +0300 Subject: [PATCH 15/21] core -> core-internal, sdk-shared -> core --- .changeset/abort-handlers-on-close.md | 2 +- .changeset/add-core-public-package.md | 5 + .changeset/add-resource-size-field.md | 2 +- .changeset/add-sdk-http-error.md | 2 +- .changeset/add-sdk-shared-package.md | 5 - .changeset/busy-weeks-hang.md | 2 +- .changeset/codemod-core-routing.md | 5 + .changeset/codemod-sdk-shared-routing.md | 5 - .changeset/custom-methods-minimal.md | 2 +- .changeset/extract-task-manager.md | 2 +- .changeset/finish-sdkerror-capability.md | 2 +- .changeset/fix-abort-listener-leak.md | 2 +- .changeset/fix-task-session-isolation.md | 2 +- ...transport-exact-optional-property-types.md | 2 +- .changeset/fix-unknown-tool-protocol-error.md | 2 +- .changeset/funky-baths-attack.md | 2 +- .changeset/idjag-spec-type-export.md | 2 +- .changeset/pre.json | 4 +- .changeset/quick-islands-occur.md | 2 +- .changeset/register-rawshape-compat.md | 2 +- .changeset/rename-sdk-shared-to-core.md | 5 + .changeset/restore-task-wire-types.md | 2 +- .changeset/rich-hounds-report.md | 2 +- .changeset/schema-object-type-for-unions.md | 2 +- .changeset/sep-2577-deprecate-runtime-apis.md | 2 +- .changeset/sep-2663-tasks-removal.md | 2 +- .changeset/sep-414-trace-context-meta-keys.md | 2 +- .changeset/shy-times-learn.md | 2 +- .changeset/spec-reference-types-2026-07-28.md | 2 +- .changeset/stdio-max-buffer-size.md | 4 +- .changeset/stdio-skip-non-json.md | 2 +- .changeset/support-standard-json-schema.md | 4 +- .changeset/workerd-shim-vendors-cfworker.md | 2 +- .changeset/wraphandler-hook.md | 2 +- .changeset/zod-json-schema-compat.md | 2 +- .changeset/zod-jsonschema-fallback.md | 2 +- .github/workflows/publish.yml | 2 +- .github/workflows/update-spec-types.yml | 8 +- CLAUDE.md | 22 +- docs/migration-SKILL.md | 16 +- docs/migration.md | 24 +- examples/client-quickstart/tsconfig.json | 8 +- examples/client/tsconfig.json | 8 +- examples/server-quickstart/tsconfig.json | 8 +- examples/server/tsconfig.json | 8 +- examples/shared/package.json | 2 +- examples/shared/tsconfig.json | 8 +- packages/client/CHANGELOG.md | 5 +- packages/client/eslint.config.mjs | 2 +- packages/client/package.json | 2 +- packages/client/src/client/auth.examples.ts | 2 +- packages/client/src/client/auth.ts | 4 +- packages/client/src/client/authExtensions.ts | 2 +- packages/client/src/client/client.examples.ts | 2 +- packages/client/src/client/client.ts | 4 +- packages/client/src/client/crossAppAccess.ts | 4 +- packages/client/src/client/middleware.ts | 2 +- packages/client/src/client/sse.ts | 4 +- packages/client/src/client/stdio.ts | 4 +- packages/client/src/client/streamableHttp.ts | 4 +- packages/client/src/fromJsonSchema.ts | 4 +- packages/client/src/index.ts | 4 +- packages/client/src/shimsBrowser.ts | 2 +- packages/client/src/shimsNode.ts | 2 +- packages/client/src/shimsWorkerd.ts | 2 +- packages/client/src/validators/ajv.ts | 2 +- packages/client/src/validators/cfWorker.ts | 4 +- packages/client/test/client/auth.test.ts | 10 +- .../client/test/client/crossAppAccess.test.ts | 2 +- .../client/test/client/crossSpawn.test.ts | 2 +- .../jsonSchemaValidatorOverride.test.ts | 4 +- .../client/test/client/middleware.test.ts | 2 +- packages/client/test/client/sse.test.ts | 4 +- packages/client/test/client/stdio.test.ts | 2 +- .../client/test/client/streamableHttp.test.ts | 4 +- .../client/test/client/tokenProvider.test.ts | 4 +- packages/client/tsconfig.json | 14 +- packages/client/tsdown.config.ts | 10 +- packages/codemod/scripts/generateVersions.ts | 2 +- packages/codemod/src/bin/batchTest.ts | 4 +- packages/codemod/src/generated/versions.ts | 2 +- .../v1-to-v2/mappings/authSchemaNames.ts | 16 +- .../migrations/v1-to-v2/mappings/importMap.ts | 10 +- .../v1-to-v2/mappings/schemaRouting.ts | 4 +- .../v1-to-v2/mappings/specSchemaNames.ts | 6 +- .../migrations/v1-to-v2/mappings/symbolMap.ts | 2 +- .../v1-to-v2/transforms/importPaths.ts | 14 +- .../v1-to-v2/transforms/mockPaths.ts | 8 +- packages/codemod/src/utils/importUtils.ts | 2 +- .../codemod/src/utils/packageJsonUpdater.ts | 2 +- packages/codemod/test/integration.test.ts | 24 +- .../codemod/test/packageJsonUpdater.test.ts | 8 +- .../test/v1-to-v2/authSchemaNames.test.ts | 16 +- .../test/v1-to-v2/specSchemaNames.test.ts | 12 +- .../v1-to-v2/transforms/importPaths.test.ts | 90 +- .../v1-to-v2/transforms/mockPaths.test.ts | 18 +- packages/core/CHANGELOG.md | 106 - packages/{sdk-shared => core}/README.md | 10 +- packages/core/eslint.config.mjs | 9 +- packages/core/package.json | 69 +- packages/core/src/auth/errors.ts | 132 - .../core/src/errors/sdkErrors.examples.ts | 39 - packages/core/src/errors/sdkErrors.ts | 110 - packages/core/src/exports/public/index.ts | 126 - packages/core/src/exports/types/index.ts | 1 - packages/core/src/index.ts | 218 +- packages/core/src/shared/auth.ts | 252 -- packages/core/src/shared/authUtils.ts | 57 - packages/core/src/shared/metadataUtils.ts | 26 - packages/core/src/shared/protocol.examples.ts | 29 - packages/core/src/shared/protocol.ts | 1066 ------ packages/core/src/shared/stdio.ts | 62 - .../core/src/shared/toolNameValidation.ts | 116 - packages/core/src/shared/transport.ts | 134 - packages/core/src/shared/uriTemplate.ts | 290 -- packages/core/src/types/constants.ts | 86 - packages/core/src/types/enums.ts | 26 - packages/core/src/types/errors.ts | 85 - packages/core/src/types/guards.ts | 110 - packages/core/src/types/index.ts | 9 - packages/core/src/types/schemas.ts | 2346 ------------- .../core/src/types/spec.types.2025-11-25.ts | 2559 -------------- .../core/src/types/spec.types.2026-07-28.ts | 3030 ----------------- .../core/src/types/specTypeSchema.examples.ts | 40 - packages/core/src/types/specTypeSchema.ts | 301 -- packages/core/src/types/types.ts | 604 ---- packages/core/src/util/inMemory.ts | 73 - packages/core/src/util/schema.ts | 32 - packages/core/src/util/standardSchema.ts | 251 -- packages/core/src/util/zodCompat.ts | 80 - .../src/validators/ajvProvider.examples.ts | 46 - packages/core/src/validators/ajvProvider.ts | 99 - .../validators/cfWorkerProvider.examples.ts | 33 - .../core/src/validators/cfWorkerProvider.ts | 81 - .../src/validators/fromJsonSchema.examples.ts | 30 - .../core/src/validators/fromJsonSchema.ts | 43 - .../core/src/validators/types.examples.ts | 31 - packages/core/src/validators/types.ts | 59 - .../test/coreSchemas.test.ts} | 13 +- .../core/test/errors/sdkHttpError.test.ts | 55 - packages/core/test/inMemory.test.ts | 165 - packages/core/test/shared/auth.test.ts | 122 - packages/core/test/shared/authUtils.test.ts | 90 - .../core/test/shared/customMethods.test.ts | 198 -- packages/core/test/shared/protocol.test.ts | 912 ----- .../shared/protocolTransportHandling.test.ts | 123 - packages/core/test/shared/stdio.test.ts | 158 - .../test/shared/toolNameValidation.test.ts | 130 - .../core/test/shared/traceContextMeta.test.ts | 120 - packages/core/test/shared/transport.test.ts | 182 - packages/core/test/shared/uriTemplate.test.ts | 314 -- packages/core/test/shared/wrapHandler.test.ts | 33 - .../core/test/spec.types.2025-11-25.test.ts | 950 ------ .../core/test/spec.types.2026-07-28.test.ts | 550 --- packages/core/test/types.capabilities.test.ts | 103 - packages/core/test/types.test.ts | 1174 ------- packages/core/test/types/errors.test.ts | 44 - packages/core/test/types/guards.test.ts | 123 - .../core/test/types/specTypeSchema.test.ts | 177 - .../core/test/util/standardSchema.test.ts | 42 - .../util/standardSchema.zodFallback.test.ts | 37 - packages/core/test/util/zodCompat.test.ts | 89 - .../core/test/validators/validators.test.ts | 625 ---- packages/core/tsconfig.json | 4 +- .../{sdk-shared => core}/tsdown.config.ts | 12 +- packages/{sdk-shared => core}/typedoc.json | 0 packages/middleware/express/tsconfig.json | 8 +- packages/middleware/fastify/tsconfig.json | 4 +- packages/middleware/hono/tsconfig.json | 8 +- packages/middleware/node/eslint.config.mjs | 2 +- packages/middleware/node/package.json | 2 +- .../node/test/streamableHttp.test.ts | 2 +- packages/middleware/node/tsconfig.json | 6 +- packages/sdk-shared/eslint.config.mjs | 12 - packages/sdk-shared/package.json | 63 - packages/sdk-shared/src/index.ts | 198 -- packages/sdk-shared/tsconfig.json | 12 - packages/sdk-shared/vitest.config.js | 3 - packages/server-legacy/eslint.config.mjs | 2 +- packages/server-legacy/package.json | 2 +- packages/server-legacy/src/auth/clients.ts | 2 +- packages/server-legacy/src/auth/errors.ts | 2 +- .../src/auth/handlers/metadata.ts | 2 +- .../src/auth/handlers/register.ts | 4 +- .../server-legacy/src/auth/handlers/revoke.ts | 2 +- .../src/auth/middleware/clientAuth.ts | 2 +- packages/server-legacy/src/auth/provider.ts | 2 +- .../src/auth/providers/proxyProvider.ts | 4 +- packages/server-legacy/src/auth/router.ts | 2 +- packages/server-legacy/src/auth/types.ts | 2 +- packages/server-legacy/src/sse/sse.ts | 4 +- .../test/auth/handlers/authorize.test.ts | 2 +- .../test/auth/handlers/metadata.test.ts | 2 +- .../test/auth/handlers/register.test.ts | 2 +- .../test/auth/handlers/revoke.test.ts | 2 +- .../test/auth/handlers/token.test.ts | 2 +- .../test/auth/middleware/clientAuth.test.ts | 2 +- .../test/auth/providers/proxyProvider.test.ts | 2 +- .../server-legacy/test/auth/router.test.ts | 2 +- packages/server-legacy/test/sse/sse.test.ts | 2 +- packages/server-legacy/tsconfig.json | 4 +- packages/server-legacy/tsdown.config.ts | 6 +- packages/server/CHANGELOG.md | 5 +- packages/server/eslint.config.mjs | 2 +- packages/server/package.json | 2 +- packages/server/src/fromJsonSchema.ts | 4 +- packages/server/src/index.ts | 4 +- packages/server/src/server/completable.ts | 2 +- packages/server/src/server/mcp.examples.ts | 2 +- packages/server/src/server/mcp.ts | 4 +- packages/server/src/server/server.ts | 4 +- packages/server/src/server/stdio.ts | 4 +- packages/server/src/server/streamableHttp.ts | 4 +- packages/server/src/shimsNode.ts | 2 +- packages/server/src/shimsWorkerd.ts | 2 +- packages/server/src/validators/ajv.ts | 2 +- packages/server/src/validators/cfWorker.ts | 4 +- .../jsonSchemaValidatorOverride.test.ts | 4 +- .../server/test/server/mcp.compat.test.ts | 4 +- packages/server/test/server/mcp.icons.test.ts | 4 +- packages/server/test/server/server.test.ts | 4 +- packages/server/test/server/stdio.test.ts | 4 +- .../server/test/server/streamableHttp.test.ts | 2 +- .../streamableHttpFutureVersionGates.test.ts | 2 +- ...mableHttpUnsupportedVersionLiteral.test.ts | 4 +- packages/server/tsconfig.json | 14 +- packages/server/tsdown.config.ts | 10 +- pnpm-lock.yaml | 134 +- scripts/fetch-spec-types.ts | 4 +- scripts/sync-snippets.ts | 2 +- test/conformance/package.json | 2 +- test/conformance/tsconfig.json | 6 +- test/e2e/helpers/wire-sniffer.ts | 2 +- test/e2e/package.json | 2 +- test/e2e/scenarios/sampling.test.ts | 2 +- test/e2e/scenarios/stdio.test.ts | 2 +- test/e2e/scenarios/tools.test.ts | 2 +- test/e2e/scenarios/transport-http.test.ts | 2 +- test/e2e/scenarios/transport-raw.test.ts | 2 +- test/e2e/scenarios/validation.test.ts | 2 +- test/e2e/tsconfig.json | 10 +- test/helpers/package.json | 2 +- test/helpers/src/helpers/oauth.ts | 2 +- test/helpers/tsconfig.json | 6 +- test/integration/package.json | 2 +- test/integration/test/client/client.test.ts | 4 +- .../test1277.zod.v4.description.test.ts | 2 +- .../test400.optional-tool-params.test.ts | 2 +- test/integration/test/server.test.ts | 4 +- .../test/server/declaredCapabilities.test.ts | 2 +- .../test/server/elicitation.test.ts | 8 +- test/integration/test/server/mcp.test.ts | 10 +- test/integration/test/standardSchema.test.ts | 4 +- test/integration/test/title.test.ts | 2 +- test/integration/tsconfig.json | 14 +- typedoc.config.mjs | 4 +- 256 files changed, 757 insertions(+), 19981 deletions(-) create mode 100644 .changeset/add-core-public-package.md delete mode 100644 .changeset/add-sdk-shared-package.md create mode 100644 .changeset/codemod-core-routing.md delete mode 100644 .changeset/codemod-sdk-shared-routing.md create mode 100644 .changeset/rename-sdk-shared-to-core.md delete mode 100644 packages/core/CHANGELOG.md rename packages/{sdk-shared => core}/README.md (85%) delete mode 100644 packages/core/src/auth/errors.ts delete mode 100644 packages/core/src/errors/sdkErrors.examples.ts delete mode 100644 packages/core/src/errors/sdkErrors.ts delete mode 100644 packages/core/src/exports/public/index.ts delete mode 100644 packages/core/src/exports/types/index.ts delete mode 100644 packages/core/src/shared/auth.ts delete mode 100644 packages/core/src/shared/authUtils.ts delete mode 100644 packages/core/src/shared/metadataUtils.ts delete mode 100644 packages/core/src/shared/protocol.examples.ts delete mode 100644 packages/core/src/shared/protocol.ts delete mode 100644 packages/core/src/shared/stdio.ts delete mode 100644 packages/core/src/shared/toolNameValidation.ts delete mode 100644 packages/core/src/shared/transport.ts delete mode 100644 packages/core/src/shared/uriTemplate.ts delete mode 100644 packages/core/src/types/constants.ts delete mode 100644 packages/core/src/types/enums.ts delete mode 100644 packages/core/src/types/errors.ts delete mode 100644 packages/core/src/types/guards.ts delete mode 100644 packages/core/src/types/index.ts delete mode 100644 packages/core/src/types/schemas.ts delete mode 100644 packages/core/src/types/spec.types.2025-11-25.ts delete mode 100644 packages/core/src/types/spec.types.2026-07-28.ts delete mode 100644 packages/core/src/types/specTypeSchema.examples.ts delete mode 100644 packages/core/src/types/specTypeSchema.ts delete mode 100644 packages/core/src/types/types.ts delete mode 100644 packages/core/src/util/inMemory.ts delete mode 100644 packages/core/src/util/schema.ts delete mode 100644 packages/core/src/util/standardSchema.ts delete mode 100644 packages/core/src/util/zodCompat.ts delete mode 100644 packages/core/src/validators/ajvProvider.examples.ts delete mode 100644 packages/core/src/validators/ajvProvider.ts delete mode 100644 packages/core/src/validators/cfWorkerProvider.examples.ts delete mode 100644 packages/core/src/validators/cfWorkerProvider.ts delete mode 100644 packages/core/src/validators/fromJsonSchema.examples.ts delete mode 100644 packages/core/src/validators/fromJsonSchema.ts delete mode 100644 packages/core/src/validators/types.examples.ts delete mode 100644 packages/core/src/validators/types.ts rename packages/{sdk-shared/test/sdkSharedSchemas.test.ts => core/test/coreSchemas.test.ts} (87%) delete mode 100644 packages/core/test/errors/sdkHttpError.test.ts delete mode 100644 packages/core/test/inMemory.test.ts delete mode 100644 packages/core/test/shared/auth.test.ts delete mode 100644 packages/core/test/shared/authUtils.test.ts delete mode 100644 packages/core/test/shared/customMethods.test.ts delete mode 100644 packages/core/test/shared/protocol.test.ts delete mode 100644 packages/core/test/shared/protocolTransportHandling.test.ts delete mode 100644 packages/core/test/shared/stdio.test.ts delete mode 100644 packages/core/test/shared/toolNameValidation.test.ts delete mode 100644 packages/core/test/shared/traceContextMeta.test.ts delete mode 100644 packages/core/test/shared/transport.test.ts delete mode 100644 packages/core/test/shared/uriTemplate.test.ts delete mode 100644 packages/core/test/shared/wrapHandler.test.ts delete mode 100644 packages/core/test/spec.types.2025-11-25.test.ts delete mode 100644 packages/core/test/spec.types.2026-07-28.test.ts delete mode 100644 packages/core/test/types.capabilities.test.ts delete mode 100644 packages/core/test/types.test.ts delete mode 100644 packages/core/test/types/errors.test.ts delete mode 100644 packages/core/test/types/guards.test.ts delete mode 100644 packages/core/test/types/specTypeSchema.test.ts delete mode 100644 packages/core/test/util/standardSchema.test.ts delete mode 100644 packages/core/test/util/standardSchema.zodFallback.test.ts delete mode 100644 packages/core/test/util/zodCompat.test.ts delete mode 100644 packages/core/test/validators/validators.test.ts rename packages/{sdk-shared => core}/tsdown.config.ts (59%) rename packages/{sdk-shared => core}/typedoc.json (100%) delete mode 100644 packages/sdk-shared/eslint.config.mjs delete mode 100644 packages/sdk-shared/package.json delete mode 100644 packages/sdk-shared/src/index.ts delete mode 100644 packages/sdk-shared/tsconfig.json delete mode 100644 packages/sdk-shared/vitest.config.js diff --git a/.changeset/abort-handlers-on-close.md b/.changeset/abort-handlers-on-close.md index b6bc65e652..c09d8b5aac 100644 --- a/.changeset/abort-handlers-on-close.md +++ b/.changeset/abort-handlers-on-close.md @@ -1,5 +1,5 @@ --- -'@modelcontextprotocol/core': patch +'@modelcontextprotocol/core-internal': patch --- Abort in-flight request handlers when the connection closes. Previously, request handlers would continue running after the transport disconnected, wasting resources and preventing proper cleanup. Also fixes `InMemoryTransport.close()` firing `onclose` twice on the initiating side. diff --git a/.changeset/add-core-public-package.md b/.changeset/add-core-public-package.md new file mode 100644 index 0000000000..23cb56cb21 --- /dev/null +++ b/.changeset/add-core-public-package.md @@ -0,0 +1,5 @@ +--- +'@modelcontextprotocol/core': minor +--- + +Add `@modelcontextprotocol/core`: the public home for the MCP specification and OAuth/OpenID Zod schemas. It bundles the SDK's internal schema definitions and re-exports only the `*Schema` values, so consumers can validate protocol payloads (`Schema.parse(value)` / `.safeParse(value)`) without depending on a package's internal barrel. Alongside the spec schemas it also re-exports the auth schemas v1 exposed from `@modelcontextprotocol/sdk/shared/auth.js` (e.g. `OAuthTokensSchema`, `OAuthMetadataSchema`, `IdJagTokenExchangeResponseSchema`). Spec types, error classes, enums, and guards continue to live on `@modelcontextprotocol/server` and `@modelcontextprotocol/client`. diff --git a/.changeset/add-resource-size-field.md b/.changeset/add-resource-size-field.md index bef37cb408..07d06b436d 100644 --- a/.changeset/add-resource-size-field.md +++ b/.changeset/add-resource-size-field.md @@ -1,5 +1,5 @@ --- -'@modelcontextprotocol/core': patch +'@modelcontextprotocol/core-internal': patch --- Add missing `size` field to `ResourceSchema` to match the MCP specification diff --git a/.changeset/add-sdk-http-error.md b/.changeset/add-sdk-http-error.md index c3331a565e..05abc0d8f1 100644 --- a/.changeset/add-sdk-http-error.md +++ b/.changeset/add-sdk-http-error.md @@ -1,5 +1,5 @@ --- -"@modelcontextprotocol/core": minor +"@modelcontextprotocol/core-internal": minor "@modelcontextprotocol/client": minor --- diff --git a/.changeset/add-sdk-shared-package.md b/.changeset/add-sdk-shared-package.md deleted file mode 100644 index e9a88426e9..0000000000 --- a/.changeset/add-sdk-shared-package.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@modelcontextprotocol/sdk-shared': minor ---- - -Add `@modelcontextprotocol/sdk-shared`: the public home for the MCP specification and OAuth/OpenID Zod schemas. It bundles the SDK's internal schema definitions and re-exports only the `*Schema` values, so consumers can validate protocol payloads (`Schema.parse(value)` / `.safeParse(value)`) without depending on a package's internal barrel. Alongside the spec schemas it also re-exports the auth schemas v1 exposed from `@modelcontextprotocol/sdk/shared/auth.js` (e.g. `OAuthTokensSchema`, `OAuthMetadataSchema`, `IdJagTokenExchangeResponseSchema`). Spec types, error classes, enums, and guards continue to live on `@modelcontextprotocol/server` and `@modelcontextprotocol/client`. diff --git a/.changeset/busy-weeks-hang.md b/.changeset/busy-weeks-hang.md index a045aaa41f..0a8801eb37 100644 --- a/.changeset/busy-weeks-hang.md +++ b/.changeset/busy-weeks-hang.md @@ -1,5 +1,5 @@ --- -'@modelcontextprotocol/core': patch +'@modelcontextprotocol/core-internal': patch '@modelcontextprotocol/server': patch --- diff --git a/.changeset/codemod-core-routing.md b/.changeset/codemod-core-routing.md new file mode 100644 index 0000000000..624a847238 --- /dev/null +++ b/.changeset/codemod-core-routing.md @@ -0,0 +1,5 @@ +--- +'@modelcontextprotocol/codemod': minor +--- + +Route v1 `@modelcontextprotocol/sdk/types.js` schema imports to the new `@modelcontextprotocol/core` package. The `*Schema` Zod constants now migrate as a behavior-preserving import-path swap — `Schema.parse(value)` / `.safeParse(value)` keep working — while spec types, error classes, enums, and guards continue to resolve to `@modelcontextprotocol/client` / `@modelcontextprotocol/server` by context. A single `import { CallToolResult, CallToolResultSchema } from '.../types.js'` is split accordingly. The v1 OAuth/OpenID `*Schema` constants imported from `@modelcontextprotocol/sdk/shared/auth.js` are routed to `@modelcontextprotocol/core` the same way (their auth TYPES keep resolving to `client` / `server`). The previous `specSchemaAccess` transform (which rewrote `.parse()` into `specTypeSchemas.X['~standard'].validate(...)`) is removed. diff --git a/.changeset/codemod-sdk-shared-routing.md b/.changeset/codemod-sdk-shared-routing.md deleted file mode 100644 index 2563314304..0000000000 --- a/.changeset/codemod-sdk-shared-routing.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@modelcontextprotocol/codemod': minor ---- - -Route v1 `@modelcontextprotocol/sdk/types.js` schema imports to the new `@modelcontextprotocol/sdk-shared` package. The `*Schema` Zod constants now migrate as a behavior-preserving import-path swap — `Schema.parse(value)` / `.safeParse(value)` keep working — while spec types, error classes, enums, and guards continue to resolve to `@modelcontextprotocol/client` / `@modelcontextprotocol/server` by context. A single `import { CallToolResult, CallToolResultSchema } from '.../types.js'` is split accordingly. The v1 OAuth/OpenID `*Schema` constants imported from `@modelcontextprotocol/sdk/shared/auth.js` are routed to `sdk-shared` the same way (their auth TYPES keep resolving to `client` / `server`). The previous `specSchemaAccess` transform (which rewrote `.parse()` into `specTypeSchemas.X['~standard'].validate(...)`) is removed. diff --git a/.changeset/custom-methods-minimal.md b/.changeset/custom-methods-minimal.md index f722f4504e..103332b166 100644 --- a/.changeset/custom-methods-minimal.md +++ b/.changeset/custom-methods-minimal.md @@ -1,5 +1,5 @@ --- -'@modelcontextprotocol/core': minor +'@modelcontextprotocol/core-internal': minor '@modelcontextprotocol/client': minor '@modelcontextprotocol/server': minor --- diff --git a/.changeset/extract-task-manager.md b/.changeset/extract-task-manager.md index 6a72182837..51ee9b305a 100644 --- a/.changeset/extract-task-manager.md +++ b/.changeset/extract-task-manager.md @@ -1,5 +1,5 @@ --- -"@modelcontextprotocol/core": minor +"@modelcontextprotocol/core-internal": minor "@modelcontextprotocol/client": minor "@modelcontextprotocol/server": minor --- diff --git a/.changeset/finish-sdkerror-capability.md b/.changeset/finish-sdkerror-capability.md index f9145a5058..8e8b0bca31 100644 --- a/.changeset/finish-sdkerror-capability.md +++ b/.changeset/finish-sdkerror-capability.md @@ -1,5 +1,5 @@ --- -'@modelcontextprotocol/core': patch +'@modelcontextprotocol/core-internal': patch '@modelcontextprotocol/client': patch '@modelcontextprotocol/server': patch --- diff --git a/.changeset/fix-abort-listener-leak.md b/.changeset/fix-abort-listener-leak.md index f1dd3163b9..0a87e559c7 100644 --- a/.changeset/fix-abort-listener-leak.md +++ b/.changeset/fix-abort-listener-leak.md @@ -1,5 +1,5 @@ --- -'@modelcontextprotocol/core': patch +'@modelcontextprotocol/core-internal': patch --- Consolidate per-request cleanup in `_requestWithSchema` into a single `.finally()` block. This fixes an abort signal listener leak (listeners accumulated when a caller reused one `AbortSignal` across requests) and two cases where `_responseHandlers` entries leaked on send-failure paths. diff --git a/.changeset/fix-task-session-isolation.md b/.changeset/fix-task-session-isolation.md index 7220673374..8f75b6004f 100644 --- a/.changeset/fix-task-session-isolation.md +++ b/.changeset/fix-task-session-isolation.md @@ -1,5 +1,5 @@ --- -'@modelcontextprotocol/core': patch +'@modelcontextprotocol/core-internal': patch --- Fix InMemoryTaskStore to enforce session isolation. Previously, sessionId was accepted but ignored on all TaskStore methods, allowing any session to enumerate, read, and mutate tasks created by other sessions. The store now persists sessionId at creation time and enforces ownership on all reads and writes. diff --git a/.changeset/fix-transport-exact-optional-property-types.md b/.changeset/fix-transport-exact-optional-property-types.md index c3187db8ad..57b4d00cd5 100644 --- a/.changeset/fix-transport-exact-optional-property-types.md +++ b/.changeset/fix-transport-exact-optional-property-types.md @@ -1,5 +1,5 @@ --- -'@modelcontextprotocol/core': patch +'@modelcontextprotocol/core-internal': patch --- Add explicit `| undefined` to optional properties on the `Transport` interface and `TransportSendOptions` (`onclose`, `onerror`, `onmessage`, `sessionId`, `setProtocolVersion`, `setSupportedProtocolVersions`, `onresumptiontoken`). diff --git a/.changeset/fix-unknown-tool-protocol-error.md b/.changeset/fix-unknown-tool-protocol-error.md index 086158b4b6..d96a628cae 100644 --- a/.changeset/fix-unknown-tool-protocol-error.md +++ b/.changeset/fix-unknown-tool-protocol-error.md @@ -1,5 +1,5 @@ --- -"@modelcontextprotocol/core": minor +"@modelcontextprotocol/core-internal": minor "@modelcontextprotocol/server": major --- diff --git a/.changeset/funky-baths-attack.md b/.changeset/funky-baths-attack.md index f65f1263c8..8f7e20f73b 100644 --- a/.changeset/funky-baths-attack.md +++ b/.changeset/funky-baths-attack.md @@ -2,7 +2,7 @@ '@modelcontextprotocol/node': patch '@modelcontextprotocol/test-integration': patch '@modelcontextprotocol/server': patch -'@modelcontextprotocol/core': patch +'@modelcontextprotocol/core-internal': patch --- remove deprecated .tool, .prompt, .resource method signatures diff --git a/.changeset/idjag-spec-type-export.md b/.changeset/idjag-spec-type-export.md index e3c9e05a03..8fb5816036 100644 --- a/.changeset/idjag-spec-type-export.md +++ b/.changeset/idjag-spec-type-export.md @@ -3,4 +3,4 @@ '@modelcontextprotocol/server': minor --- -Add the v2 `IdJagTokenExchangeResponse` type to the public API and register its schema as an MCP spec type. `IdJagTokenExchangeResponse` is now exported from `@modelcontextprotocol/client` and `@modelcontextprotocol/server`, `'IdJagTokenExchangeResponse'` joins the `SpecTypeName` union, and `isSpecType.IdJagTokenExchangeResponse(value)` / `specTypeSchemas.IdJagTokenExchangeResponse` validate it by name — matching how the OAuth/OpenID auth schemas are already exposed. The Zod schema itself, `IdJagTokenExchangeResponseSchema`, is available from `@modelcontextprotocol/sdk-shared`. +Add the v2 `IdJagTokenExchangeResponse` type to the public API and register its schema as an MCP spec type. `IdJagTokenExchangeResponse` is now exported from `@modelcontextprotocol/client` and `@modelcontextprotocol/server`, `'IdJagTokenExchangeResponse'` joins the `SpecTypeName` union, and `isSpecType.IdJagTokenExchangeResponse(value)` / `specTypeSchemas.IdJagTokenExchangeResponse` validate it by name — matching how the OAuth/OpenID auth schemas are already exposed. The Zod schema itself, `IdJagTokenExchangeResponseSchema`, is available from `@modelcontextprotocol/core`. diff --git a/.changeset/pre.json b/.changeset/pre.json index 9f29aaaee2..bcaf0539de 100644 --- a/.changeset/pre.json +++ b/.changeset/pre.json @@ -11,14 +11,14 @@ "@modelcontextprotocol/examples-server-quickstart": "2.0.0-alpha.0", "@modelcontextprotocol/examples-shared": "2.0.0-alpha.0", "@modelcontextprotocol/client": "2.0.0-alpha.0", - "@modelcontextprotocol/core": "2.0.0-alpha.0", + "@modelcontextprotocol/core-internal": "2.0.0-alpha.0", "@modelcontextprotocol/express": "2.0.0-alpha.0", "@modelcontextprotocol/fastify": "2.0.0-alpha.0", "@modelcontextprotocol/hono": "2.0.0-alpha.0", "@modelcontextprotocol/node": "2.0.0-alpha.0", "@modelcontextprotocol/server": "2.0.0-alpha.0", "@modelcontextprotocol/server-legacy": "2.0.0-alpha.0", - "@modelcontextprotocol/sdk-shared": "2.0.0-alpha.0", + "@modelcontextprotocol/core": "2.0.0-alpha.0", "@modelcontextprotocol/codemod": "2.0.0-alpha.0", "@modelcontextprotocol/test-conformance": "2.0.0-alpha.0", "@modelcontextprotocol/test-helpers": "2.0.0-alpha.0", diff --git a/.changeset/quick-islands-occur.md b/.changeset/quick-islands-occur.md index 2ec83908d1..b229b82c36 100644 --- a/.changeset/quick-islands-occur.md +++ b/.changeset/quick-islands-occur.md @@ -4,7 +4,7 @@ '@modelcontextprotocol/node': patch '@modelcontextprotocol/client': patch '@modelcontextprotocol/server': patch -'@modelcontextprotocol/core': patch +'@modelcontextprotocol/core-internal': patch --- remove npm references, use pnpm diff --git a/.changeset/register-rawshape-compat.md b/.changeset/register-rawshape-compat.md index 5f1f167848..84fccdd726 100644 --- a/.changeset/register-rawshape-compat.md +++ b/.changeset/register-rawshape-compat.md @@ -1,5 +1,5 @@ --- -'@modelcontextprotocol/core': patch +'@modelcontextprotocol/core-internal': patch '@modelcontextprotocol/server': patch --- diff --git a/.changeset/rename-sdk-shared-to-core.md b/.changeset/rename-sdk-shared-to-core.md new file mode 100644 index 0000000000..ba57d09b06 --- /dev/null +++ b/.changeset/rename-sdk-shared-to-core.md @@ -0,0 +1,5 @@ +--- +'@modelcontextprotocol/core': minor +--- + +The public Zod-schema package previously published (in alpha) as `@modelcontextprotocol/sdk-shared` is now `@modelcontextprotocol/core`. Update imports: `@modelcontextprotocol/sdk-shared` → `@modelcontextprotocol/core`. The v1→v2 codemod emits the new name automatically. (The private internal barrel formerly named `@modelcontextprotocol/core` is now `@modelcontextprotocol/core-internal` and is not published.) diff --git a/.changeset/restore-task-wire-types.md b/.changeset/restore-task-wire-types.md index ebe9b5ddf5..05c0875364 100644 --- a/.changeset/restore-task-wire-types.md +++ b/.changeset/restore-task-wire-types.md @@ -1,5 +1,5 @@ --- -'@modelcontextprotocol/core': minor +'@modelcontextprotocol/core-internal': minor '@modelcontextprotocol/server': minor '@modelcontextprotocol/client': minor --- diff --git a/.changeset/rich-hounds-report.md b/.changeset/rich-hounds-report.md index d1736bf72c..b05e7c4656 100644 --- a/.changeset/rich-hounds-report.md +++ b/.changeset/rich-hounds-report.md @@ -4,7 +4,7 @@ '@modelcontextprotocol/node': patch '@modelcontextprotocol/client': patch '@modelcontextprotocol/server': patch -'@modelcontextprotocol/core': patch +'@modelcontextprotocol/core-internal': patch --- clean up package manager usage, all pnpm diff --git a/.changeset/schema-object-type-for-unions.md b/.changeset/schema-object-type-for-unions.md index 7749bb6c5c..d307f2af58 100644 --- a/.changeset/schema-object-type-for-unions.md +++ b/.changeset/schema-object-type-for-unions.md @@ -1,5 +1,5 @@ --- -'@modelcontextprotocol/core': patch +'@modelcontextprotocol/core-internal': patch --- Ensure `standardSchemaToJsonSchema` emits `type: "object"` at the root, fixing discriminated-union tool/prompt schemas that previously produced `{oneOf: [...]}` without the MCP-required top-level type. Also throws a clear error when given an explicitly non-object schema (e.g. `z.string()`). Fixes #1643. diff --git a/.changeset/sep-2577-deprecate-runtime-apis.md b/.changeset/sep-2577-deprecate-runtime-apis.md index 60afaaceb2..e042112d08 100644 --- a/.changeset/sep-2577-deprecate-runtime-apis.md +++ b/.changeset/sep-2577-deprecate-runtime-apis.md @@ -1,5 +1,5 @@ --- -'@modelcontextprotocol/core': patch +'@modelcontextprotocol/core-internal': patch '@modelcontextprotocol/server': patch '@modelcontextprotocol/client': patch --- diff --git a/.changeset/sep-2663-tasks-removal.md b/.changeset/sep-2663-tasks-removal.md index 51ece994a4..8316165b50 100644 --- a/.changeset/sep-2663-tasks-removal.md +++ b/.changeset/sep-2663-tasks-removal.md @@ -1,5 +1,5 @@ --- -'@modelcontextprotocol/core': major +'@modelcontextprotocol/core-internal': major '@modelcontextprotocol/server': major '@modelcontextprotocol/client': major --- diff --git a/.changeset/sep-414-trace-context-meta-keys.md b/.changeset/sep-414-trace-context-meta-keys.md index f8f21b63aa..1281c9dafb 100644 --- a/.changeset/sep-414-trace-context-meta-keys.md +++ b/.changeset/sep-414-trace-context-meta-keys.md @@ -1,5 +1,5 @@ --- -'@modelcontextprotocol/core': patch +'@modelcontextprotocol/core-internal': patch --- Add reserved trace context `_meta` key constants (`TRACEPARENT_META_KEY`, `TRACESTATE_META_KEY`, `BAGGAGE_META_KEY`) per SEP-414, plus docs and a passthrough regression test. The spec reserves the unprefixed `_meta` keys `traceparent`, `tracestate`, and `baggage` (W3C Trace Context / W3C Baggage formats) for distributed tracing; the SDK passes them through untouched. diff --git a/.changeset/shy-times-learn.md b/.changeset/shy-times-learn.md index 99617f8b7d..24d47206eb 100644 --- a/.changeset/shy-times-learn.md +++ b/.changeset/shy-times-learn.md @@ -2,7 +2,7 @@ '@modelcontextprotocol/node': patch '@modelcontextprotocol/test-integration': patch '@modelcontextprotocol/server': patch -'@modelcontextprotocol/core': patch +'@modelcontextprotocol/core-internal': patch --- deprecated .tool, .prompt, .resource method removal diff --git a/.changeset/spec-reference-types-2026-07-28.md b/.changeset/spec-reference-types-2026-07-28.md index df0101e05f..3c2f5025a0 100644 --- a/.changeset/spec-reference-types-2026-07-28.md +++ b/.changeset/spec-reference-types-2026-07-28.md @@ -1,5 +1,5 @@ --- -'@modelcontextprotocol/core': patch +'@modelcontextprotocol/core-internal': patch '@modelcontextprotocol/codemod': patch --- diff --git a/.changeset/stdio-max-buffer-size.md b/.changeset/stdio-max-buffer-size.md index a4b71e9dcd..33a8c42d6d 100644 --- a/.changeset/stdio-max-buffer-size.md +++ b/.changeset/stdio-max-buffer-size.md @@ -1,7 +1,7 @@ --- -'@modelcontextprotocol/core': patch +'@modelcontextprotocol/core-internal': patch '@modelcontextprotocol/client': patch '@modelcontextprotocol/server': patch --- -Add a configurable `maxBufferSize` (default 10 MB) to the stdio transports. When a single message would push the read buffer past the limit, the transport now emits an `onerror` and closes instead of growing the buffer unbounded. Configure via `new StdioClientTransport({ ..., maxBufferSize })` or `new StdioServerTransport(stdin, stdout, { maxBufferSize })`. The default is exported from `@modelcontextprotocol/core` as `STDIO_DEFAULT_MAX_BUFFER_SIZE`. +Add a configurable `maxBufferSize` (default 10 MB) to the stdio transports. When a single message would push the read buffer past the limit, the transport now emits an `onerror` and closes instead of growing the buffer unbounded. Configure via `new StdioClientTransport({ ..., maxBufferSize })` or `new StdioServerTransport(stdin, stdout, { maxBufferSize })`. The default is exported from `@modelcontextprotocol/core-internal` as `STDIO_DEFAULT_MAX_BUFFER_SIZE`. diff --git a/.changeset/stdio-skip-non-json.md b/.changeset/stdio-skip-non-json.md index d20b740c9c..7c70e412f3 100644 --- a/.changeset/stdio-skip-non-json.md +++ b/.changeset/stdio-skip-non-json.md @@ -1,5 +1,5 @@ --- -'@modelcontextprotocol/core': patch +'@modelcontextprotocol/core-internal': patch --- `ReadBuffer.readMessage()` now silently skips non-JSON lines instead of throwing `SyntaxError`. This prevents noisy `onerror` callbacks when hot-reload tools (tsx, nodemon) write debug output like "Gracefully restarting..." to stdout. Lines that parse as JSON but fail JSONRPC schema validation still throw. diff --git a/.changeset/support-standard-json-schema.md b/.changeset/support-standard-json-schema.md index 792e15f1f7..38a9998893 100644 --- a/.changeset/support-standard-json-schema.md +++ b/.changeset/support-standard-json-schema.md @@ -1,5 +1,5 @@ --- -'@modelcontextprotocol/core': minor +'@modelcontextprotocol/core-internal': minor '@modelcontextprotocol/server': minor '@modelcontextprotocol/client': minor --- @@ -30,5 +30,5 @@ server.registerTool('greet', { **Breaking changes:** - `experimental.tasks.getTaskResult()` no longer accepts a `resultSchema` parameter. Returns `GetTaskPayloadResult` (a loose `Result`); cast to the expected type at the call site. -- Removed unused exports from `@modelcontextprotocol/core`: `SchemaInput`, `schemaToJson`, `parseSchemaAsync`, `getSchemaShape`, `getSchemaDescription`, `isOptionalSchema`, `unwrapOptionalSchema`. Use the new `standardSchemaToJsonSchema` and `validateStandardSchema` instead. +- Removed unused exports from `@modelcontextprotocol/core-internal`: `SchemaInput`, `schemaToJson`, `parseSchemaAsync`, `getSchemaShape`, `getSchemaDescription`, `isOptionalSchema`, `unwrapOptionalSchema`. Use the new `standardSchemaToJsonSchema` and `validateStandardSchema` instead. - `completable()` remains Zod-specific (it relies on Zod's `.shape` introspection). diff --git a/.changeset/workerd-shim-vendors-cfworker.md b/.changeset/workerd-shim-vendors-cfworker.md index 9759e73009..38c8222913 100644 --- a/.changeset/workerd-shim-vendors-cfworker.md +++ b/.changeset/workerd-shim-vendors-cfworker.md @@ -1,5 +1,5 @@ --- -'@modelcontextprotocol/core': minor +'@modelcontextprotocol/core-internal': minor '@modelcontextprotocol/client': patch '@modelcontextprotocol/server': patch --- diff --git a/.changeset/wraphandler-hook.md b/.changeset/wraphandler-hook.md index 935f576588..114ebf0332 100644 --- a/.changeset/wraphandler-hook.md +++ b/.changeset/wraphandler-hook.md @@ -1,5 +1,5 @@ --- -'@modelcontextprotocol/core': patch +'@modelcontextprotocol/core-internal': patch '@modelcontextprotocol/client': patch '@modelcontextprotocol/server': patch --- diff --git a/.changeset/zod-json-schema-compat.md b/.changeset/zod-json-schema-compat.md index 5ca1470e82..b3180fb06b 100644 --- a/.changeset/zod-json-schema-compat.md +++ b/.changeset/zod-json-schema-compat.md @@ -1,5 +1,5 @@ --- -'@modelcontextprotocol/core': patch +'@modelcontextprotocol/core-internal': patch --- Allow additional JSON Schema properties in elicitInput's requestedSchema type by adding .catchall(z.unknown()), matching the pattern used by inputSchema. This fixes type incompatibility when using Zod v4's .toJSONSchema() output which includes extra properties like $schema and additionalProperties. diff --git a/.changeset/zod-jsonschema-fallback.md b/.changeset/zod-jsonschema-fallback.md index e2936cf568..a02662da12 100644 --- a/.changeset/zod-jsonschema-fallback.md +++ b/.changeset/zod-jsonschema-fallback.md @@ -1,5 +1,5 @@ --- -'@modelcontextprotocol/core': patch +'@modelcontextprotocol/core-internal': patch '@modelcontextprotocol/server': patch '@modelcontextprotocol/client': patch --- diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 6d184c9867..829b634bdc 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -39,5 +39,5 @@ jobs: - name: Publish preview packages run: - pnpm dlx pkg-pr-new publish --packageManager=npm --pnpm './packages/server' './packages/server-legacy' './packages/client' + pnpm dlx pkg-pr-new publish --packageManager=npm --pnpm './packages/core' './packages/server' './packages/server-legacy' './packages/client' './packages/codemod' './packages/middleware/express' './packages/middleware/fastify' './packages/middleware/hono' './packages/middleware/node' diff --git a/.github/workflows/update-spec-types.yml b/.github/workflows/update-spec-types.yml index 482fb04213..bd832bf691 100644 --- a/.github/workflows/update-spec-types.yml +++ b/.github/workflows/update-spec-types.yml @@ -39,11 +39,11 @@ jobs: - name: Check for changes id: check_changes run: | - if git diff --quiet packages/core/src/types/spec.types.2026-07-28.ts; then + if git diff --quiet packages/core-internal/src/types/spec.types.2026-07-28.ts; then echo "has_changes=false" >> $GITHUB_OUTPUT else echo "has_changes=true" >> $GITHUB_OUTPUT - LATEST_SHA=$(grep "Last updated from commit:" packages/core/src/types/spec.types.2026-07-28.ts | cut -d: -f2 | tr -d ' ') + LATEST_SHA=$(grep "Last updated from commit:" packages/core-internal/src/types/spec.types.2026-07-28.ts | cut -d: -f2 | tr -d ' ') echo "sha=$LATEST_SHA" >> $GITHUB_OUTPUT fi @@ -59,12 +59,12 @@ jobs: git config user.email "github-actions[bot]@users.noreply.github.com" git checkout -B update-spec-types - git add packages/core/src/types/spec.types.2026-07-28.ts + git add packages/core-internal/src/types/spec.types.2026-07-28.ts git commit -m "chore: update spec.types.2026-07-28.ts from upstream" git push -f --no-verify origin update-spec-types # Create PR if it doesn't exist, or update if it does - PR_BODY="This PR updates \`packages/core/src/types/spec.types.2026-07-28.ts\` from the Model Context Protocol specification. + PR_BODY="This PR updates \`packages/core-internal/src/types/spec.types.2026-07-28.ts\` from the Model Context Protocol specification. Source file: https://github.com/modelcontextprotocol/modelcontextprotocol/blob/${{ steps.check_changes.outputs.sha }}/schema/draft/schema.ts diff --git a/CLAUDE.md b/CLAUDE.md index 8876b0bfb1..64f3741920 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -16,10 +16,10 @@ pnpm check:all # typecheck + lint across all packages # Run a single package script (examples) # Run a single package script from the repo root with pnpm filter -pnpm --filter @modelcontextprotocol/core test # vitest run (core) -pnpm --filter @modelcontextprotocol/core test:watch # vitest (watch) -pnpm --filter @modelcontextprotocol/core test -- path/to/file.test.ts -pnpm --filter @modelcontextprotocol/core test -- -t "test name" +pnpm --filter @modelcontextprotocol/core-internal test # vitest run (core) +pnpm --filter @modelcontextprotocol/core-internal test:watch # vitest (watch) +pnpm --filter @modelcontextprotocol/core-internal test -- path/to/file.test.ts +pnpm --filter @modelcontextprotocol/core-internal test -- -t "test name" ``` ## Breaking Changes @@ -53,9 +53,9 @@ Run `pnpm sync:snippets` to sync example content into JSDoc comments and markdow The SDK is organized into three main layers: -1. **Types Layer** (`packages/core/src/types/types.ts`) - Protocol types generated from the MCP specification. All JSON-RPC message types, schemas, and protocol constants are defined here using Zod v4. +1. **Types Layer** (`packages/core-internal/src/types/types.ts`) - Protocol types generated from the MCP specification. All JSON-RPC message types, schemas, and protocol constants are defined here using Zod v4. -2. **Protocol Layer** (`packages/core/src/shared/protocol.ts`) - The abstract `Protocol` class that handles JSON-RPC message routing, request/response correlation, capability negotiation, and transport management. Both `Client` and `Server` extend this class. +2. **Protocol Layer** (`packages/core-internal/src/shared/protocol.ts`) - The abstract `Protocol` class that handles JSON-RPC message routing, request/response correlation, capability negotiation, and transport management. Both `Client` and `Server` extend this class. 3. **High-Level APIs**: - `Client` (`packages/client/src/client/client.ts`) - Client implementation extending Protocol with typed methods for MCP operations @@ -66,8 +66,8 @@ The SDK is organized into three main layers: The SDK has a two-layer export structure to separate internal code from the public API: -- **`@modelcontextprotocol/core`** (main entry, `packages/core/src/index.ts`) — Internal barrel. Exports everything (including Zod schemas, Protocol class, stdio utils). Only consumed by sibling packages within the monorepo (`private: true`). -- **`@modelcontextprotocol/core/public`** (`packages/core/src/exports/public/index.ts`) — Curated public API. Exports only TypeScript types, error classes, constants, and guards. Re-exported by client and server packages. +- **`@modelcontextprotocol/core-internal`** (main entry, `packages/core-internal/src/index.ts`) — Internal barrel. Exports everything (including Zod schemas, Protocol class, stdio utils). Only consumed by sibling packages within the monorepo (`private: true`). +- **`@modelcontextprotocol/core-internal/public`** (`packages/core-internal/src/exports/public/index.ts`) — Curated public API. Exports only TypeScript types, error classes, constants, and guards. Re-exported by client and server packages. - **`@modelcontextprotocol/client`** and **`@modelcontextprotocol/server`** (`packages/*/src/index.ts`) — Final public surface. Package-specific exports (named explicitly) plus re-exports from `core/public`. When modifying exports: @@ -78,7 +78,7 @@ When modifying exports: ### Transport System -Transports (`packages/core/src/shared/transport.ts`) provide the communication layer: +Transports (`packages/core-internal/src/shared/transport.ts`) provide the communication layer: - **Streamable HTTP** (`packages/server/src/server/streamableHttp.ts`, `packages/client/src/client/streamableHttp.ts`) - Recommended transport for remote servers, supports SSE for streaming - **SSE** (`packages/server/src/server/sse.ts`, `packages/client/src/client/sse.ts`) - Legacy HTTP+SSE transport for backwards compatibility @@ -110,11 +110,11 @@ Located in `packages/*/src/experimental/`. Currently empty. The SDK uses `zod/v4` internally. Schema utilities live in: -- `packages/core/src/util/schema.ts` - AnySchema alias and helpers for inspecting Zod objects +- `packages/core-internal/src/util/schema.ts` - AnySchema alias and helpers for inspecting Zod objects ### Validation -Pluggable JSON Schema validation (`packages/core/src/validators/`): +Pluggable JSON Schema validation (`packages/core-internal/src/validators/`): - `ajvProvider.ts` - Default Ajv-based validator - `cfWorkerProvider.ts` - Cloudflare Workers-compatible alternative diff --git a/docs/migration-SKILL.md b/docs/migration-SKILL.md index cf5061733e..85ffb807e9 100644 --- a/docs/migration-SKILL.md +++ b/docs/migration-SKILL.md @@ -1,6 +1,6 @@ --- name: migrate-v1-to-v2 -description: Migrate MCP TypeScript SDK code from v1 (@modelcontextprotocol/sdk) to v2 (@modelcontextprotocol/core, /client, /server). Use when a user asks to migrate, upgrade, or port their MCP TypeScript code from v1 to v2. +description: Migrate MCP TypeScript SDK code from v1 (@modelcontextprotocol/sdk) to v2 (@modelcontextprotocol/core-internal, /client, /server). Use when a user asks to migrate, upgrade, or port their MCP TypeScript code from v1 to v2. --- # MCP TypeScript SDK: v1 → v2 Migration @@ -28,7 +28,7 @@ npm uninstall @modelcontextprotocol/sdk | Server + Express | `npm install @modelcontextprotocol/server @modelcontextprotocol/express` | | Server + Hono | `npm install @modelcontextprotocol/server @modelcontextprotocol/hono` | -`@modelcontextprotocol/core` is installed automatically as a dependency. +`@modelcontextprotocol/core-internal` is installed automatically as a dependency. ## 3. Import Mapping @@ -61,16 +61,16 @@ Replace all `@modelcontextprotocol/sdk/...` imports using this table. | v1 import path | v2 package | | ------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `@modelcontextprotocol/sdk/types.js` | Types / error classes / enums / guards → `@modelcontextprotocol/client` or `@modelcontextprotocol/server`; Zod `*Schema` constants → `@modelcontextprotocol/sdk-shared` | +| `@modelcontextprotocol/sdk/types.js` | Types / error classes / enums / guards → `@modelcontextprotocol/client` or `@modelcontextprotocol/server`; Zod `*Schema` constants → `@modelcontextprotocol/core` | | `@modelcontextprotocol/sdk/shared/protocol.js` | `@modelcontextprotocol/client` or `@modelcontextprotocol/server` | | `@modelcontextprotocol/sdk/shared/transport.js` | `@modelcontextprotocol/client` or `@modelcontextprotocol/server` | | `@modelcontextprotocol/sdk/shared/uriTemplate.js` | `@modelcontextprotocol/client` or `@modelcontextprotocol/server` | -| `@modelcontextprotocol/sdk/shared/auth.js` | Types / classes → `@modelcontextprotocol/client` or `@modelcontextprotocol/server`; OAuth/OpenID Zod `*Schema` constants (e.g. `OAuthTokensSchema`, `OAuthMetadataSchema`) → `@modelcontextprotocol/sdk-shared` | +| `@modelcontextprotocol/sdk/shared/auth.js` | Types / classes → `@modelcontextprotocol/client` or `@modelcontextprotocol/server`; OAuth/OpenID Zod `*Schema` constants (e.g. `OAuthTokensSchema`, `OAuthMetadataSchema`) → `@modelcontextprotocol/core` | | `@modelcontextprotocol/sdk/shared/stdio.js` | `@modelcontextprotocol/client` or `@modelcontextprotocol/server` (`ReadBuffer`, `serializeMessage`, `deserializeMessage` are in the root barrel; the `./stdio` subpath only has the transport class) | Notes: -- `@modelcontextprotocol/client` and `@modelcontextprotocol/server` both re-export shared types from `@modelcontextprotocol/core`, so import from whichever package you already depend on. Do not import from `@modelcontextprotocol/core` directly — it is an internal package. +- `@modelcontextprotocol/client` and `@modelcontextprotocol/server` both re-export shared types from `@modelcontextprotocol/core-internal`, so import from whichever package you already depend on. Do not import from `@modelcontextprotocol/core-internal` directly — it is an internal package. - When multiple v1 imports map to the same v2 package, consolidate them into a single import statement. ## 4. Renamed Symbols @@ -87,7 +87,7 @@ Notes: | `JSONRPCErrorSchema` | `JSONRPCErrorResponseSchema` | | `isJSONRPCError` | `isJSONRPCErrorResponse` | | `isJSONRPCResponse` (deprecated in v1) | `isJSONRPCResultResponse` (**not** v2's new `isJSONRPCResponse`, which correctly matches both result and error) | -| `JSONRPCResponseSchema` (result-only in v1) | `JSONRPCResultResponseSchema` (from `@modelcontextprotocol/sdk-shared`; **not** v2's new `JSONRPCResponseSchema`, a `z.union` that also accepts error responses) | +| `JSONRPCResponseSchema` (result-only in v1) | `JSONRPCResultResponseSchema` (from `@modelcontextprotocol/core`; **not** v2's new `JSONRPCResponseSchema`, a `z.union` that also accepts error responses) | | `ResourceReference` | `ResourceTemplateReference` | | `ResourceReferenceSchema` | `ResourceTemplateReferenceSchema` | | `IsomorphicHeaders` | REMOVED (use Web Standard `Headers`) | @@ -100,7 +100,7 @@ Notes: | `WebSocketClientTransport` | REMOVED (use `StreamableHTTPClientTransport` or `StdioClientTransport`) | All other **type** symbols from `@modelcontextprotocol/sdk/types.js` retain their original names — import them from `@modelcontextprotocol/client` or `@modelcontextprotocol/server`. The **Zod schemas** (e.g., `CallToolResultSchema`, `ListToolsResultSchema`) move to -`@modelcontextprotocol/sdk-shared`; `Schema.parse(value)` / `.safeParse(value)` keep working unchanged (the codemod rewrites the import path). To validate **without** depending on Zod, use `isSpecType.TypeName(value)` (e.g., `isSpecType.CallToolResult(v)`) or +`@modelcontextprotocol/core`; `Schema.parse(value)` / `.safeParse(value)` keep working unchanged (the codemod rewrites the import path). To validate **without** depending on Zod, use `isSpecType.TypeName(value)` (e.g., `isSpecType.CallToolResult(v)`) or `specTypeSchemas.TypeName` (a `StandardSchemaV1Sync` validator) from `@modelcontextprotocol/client` / `@modelcontextprotocol/server`; the keys are typed as `SpecTypeName`, a literal union of all spec type names. ### Error class changes @@ -304,7 +304,7 @@ Note: the third argument (`metadata`) is required — pass `{}` if no metadata. ### Removed core exports -| Removed from `@modelcontextprotocol/core` | Replacement | +| Removed from `@modelcontextprotocol/core-internal` | Replacement | | ------------------------------------------------------------------------------------ | ----------------------------------------- | | `schemaToJson(schema)` | `standardSchemaToJsonSchema(schema)` | | `parseSchemaAsync(schema, data)` | `validateStandardSchema(schema, data)` | diff --git a/docs/migration.md b/docs/migration.md index 468b1844ce..16340df04b 100644 --- a/docs/migration.md +++ b/docs/migration.md @@ -4,7 +4,7 @@ This guide covers the breaking changes introduced in v2 of the MCP TypeScript SD ## Overview -Version 2 of the MCP TypeScript SDK introduces several breaking changes to improve modularity, reduce dependency bloat, and provide a cleaner API surface. The biggest change is the split from a single `@modelcontextprotocol/sdk` package into separate `@modelcontextprotocol/core`, +Version 2 of the MCP TypeScript SDK introduces several breaking changes to improve modularity, reduce dependency bloat, and provide a cleaner API surface. The biggest change is the split from a single `@modelcontextprotocol/sdk` package into separate `@modelcontextprotocol/core-internal`, `@modelcontextprotocol/client`, and `@modelcontextprotocol/server` packages. > **Formatting:** The `@modelcontextprotocol/codemod` package automates most of the mechanical changes below, but it rewrites your code's AST without reformatting it — wrapped schemas and generated handler method strings may not match your project's style. After migrating (with @@ -18,7 +18,7 @@ The single `@modelcontextprotocol/sdk` package has been split into three package | v1 | v2 | | --------------------------- | ---------------------------------------------------------- | -| `@modelcontextprotocol/sdk` | `@modelcontextprotocol/core` (types, protocol, transports) | +| `@modelcontextprotocol/sdk` | `@modelcontextprotocol/core-internal` (types, protocol, transports) | | | `@modelcontextprotocol/client` (client implementation) | | | `@modelcontextprotocol/server` (server implementation) | @@ -33,7 +33,7 @@ npm install @modelcontextprotocol/client # If you only need a server npm install @modelcontextprotocol/server -# Both packages depend on @modelcontextprotocol/core automatically +# Both packages depend on @modelcontextprotocol/core-internal automatically ``` Update your imports accordingly: @@ -62,7 +62,7 @@ import { StdioServerTransport } from '@modelcontextprotocol/server/stdio'; import { NodeStreamableHTTPServerTransport } from '@modelcontextprotocol/node'; ``` -Note: `@modelcontextprotocol/client` and `@modelcontextprotocol/server` both re-export shared types from `@modelcontextprotocol/core`, so you can import types and error classes from whichever package you already depend on. Do not import from `@modelcontextprotocol/core` directly +Note: `@modelcontextprotocol/client` and `@modelcontextprotocol/server` both re-export shared types from `@modelcontextprotocol/core-internal`, so you can import types and error classes from whichever package you already depend on. Do not import from `@modelcontextprotocol/core-internal` directly — it is an internal package. ### Dropped Node.js 18 and CommonJS @@ -307,7 +307,7 @@ This applies to: - `outputSchema` in `registerTool()` - `argsSchema` in `registerPrompt()` -**Removed Zod-specific helpers** from `@modelcontextprotocol/core` (use Standard Schema equivalents): +**Removed Zod-specific helpers** from `@modelcontextprotocol/core-internal` (use Standard Schema equivalents): | Removed | Replacement | | ------------------------------------------------------------------------------------ | ----------------------------------------------------------------- | @@ -517,7 +517,7 @@ The return type is now inferred from the method name via `ResultTypeMap`. For ex For **custom (non-spec)** methods, keep the result-schema argument — see [Sending custom-method requests](#sending-custom-method-requests). Only drop the schema when calling a spec method. -If you were using `CallToolResultSchema` (or any `*Schema` constant) for **runtime validation** (not just in `request()`/`callTool()` calls), import the schema from `@modelcontextprotocol/sdk-shared`. Your `.parse()` / `.safeParse()` calls keep working unchanged — only the import +If you were using `CallToolResultSchema` (or any `*Schema` constant) for **runtime validation** (not just in `request()`/`callTool()` calls), import the schema from `@modelcontextprotocol/core`. Your `.parse()` / `.safeParse()` calls keep working unchanged — only the import path changes: ```typescript @@ -528,14 +528,14 @@ if (CallToolResultSchema.safeParse(value).success) { } // v2 — same code, new import path -import { CallToolResultSchema } from '@modelcontextprotocol/sdk-shared'; +import { CallToolResultSchema } from '@modelcontextprotocol/core'; if (CallToolResultSchema.safeParse(value).success) { /* ... */ } ``` -`@modelcontextprotocol/sdk-shared` is the canonical home for the Zod schema constants — both the spec schemas and the OAuth/OpenID schemas (e.g. `OAuthTokensSchema`, `OAuthMetadataSchema`) that v1 exported from `@modelcontextprotocol/sdk/shared/auth.js`. -`@modelcontextprotocol/server` and `@modelcontextprotocol/client` keep a Zod-free public surface (they export the corresponding TypeScript types, e.g. `OAuthTokens`), so the raw `*Schema` constants live in `sdk-shared`. (The codemod rewrites these imports for you.) +`@modelcontextprotocol/core` is the canonical home for the Zod schema constants — both the spec schemas and the OAuth/OpenID schemas (e.g. `OAuthTokensSchema`, `OAuthMetadataSchema`) that v1 exported from `@modelcontextprotocol/sdk/shared/auth.js`. +`@modelcontextprotocol/server` and `@modelcontextprotocol/client` keep a Zod-free public surface (they export the corresponding TypeScript types, e.g. `OAuthTokens`), so the raw `*Schema` constants live in `core`. (The codemod rewrites these imports for you.) If you'd rather **not** depend on Zod, `@modelcontextprotocol/client` and `@modelcontextprotocol/server` also expose Zod-free validators keyed by `SpecTypeName` — a literal union of every named spec type, so you get autocomplete and a compile error on typos: @@ -584,7 +584,7 @@ import { InMemoryTransport } from '@modelcontextprotocol/client'; ### Removed type aliases and deprecated exports -The following deprecated type aliases have been removed from `@modelcontextprotocol/core`: +The following deprecated type aliases have been removed from `@modelcontextprotocol/core-internal`: | Removed | Replacement | | ---------------------------------------- | ------------------------------------------------------------------------------------------------- | @@ -598,13 +598,13 @@ The following deprecated type aliases have been removed from `@modelcontextproto | `AuthInfo` (from `server/auth/types.js`) | `AuthInfo` (now re-exported by `@modelcontextprotocol/client` and `@modelcontextprotocol/server`) | All other symbols exported from `@modelcontextprotocol/sdk/types.js` retain their original names. Import the **types**, error classes, enums, and guards from `@modelcontextprotocol/client` or `@modelcontextprotocol/server`, and the **Zod schemas** (the `*Schema` constants) from -`@modelcontextprotocol/sdk-shared`. +`@modelcontextprotocol/core`. > **Note on `isJSONRPCResponse`:** v1's `isJSONRPCResponse` was a deprecated alias that only checked for _result_ responses (it was equivalent to `isJSONRPCResultResponse`). v2 removes the deprecated alias and introduces a **new** `isJSONRPCResponse` with corrected semantics — it > checks for _any_ response (either result or error). If you are migrating v1 code that used `isJSONRPCResponse`, rename it to `isJSONRPCResultResponse` to preserve the original behavior. Use the new `isJSONRPCResponse` only when you want to match both result and error responses. > **Note on `JSONRPCResponseSchema`:** the Zod schema follows the same pattern. v1's `JSONRPCResponseSchema` validated only _result_ responses; v2 reuses the name for a `z.union([JSONRPCResultResponseSchema, JSONRPCErrorResponseSchema])` that also accepts error responses. If you -> are migrating v1 code that called `JSONRPCResponseSchema.parse()`/`.safeParse()`, rename it to `JSONRPCResultResponseSchema` (re-exported by `@modelcontextprotocol/sdk-shared`) to preserve the original validation. The codemod performs this rename automatically. +> are migrating v1 code that called `JSONRPCResponseSchema.parse()`/`.safeParse()`, rename it to `JSONRPCResultResponseSchema` (re-exported by `@modelcontextprotocol/core`) to preserve the original validation. The codemod performs this rename automatically. **Before (v1):** diff --git a/examples/client-quickstart/tsconfig.json b/examples/client-quickstart/tsconfig.json index a2bf4fd724..cf326c78d8 100644 --- a/examples/client-quickstart/tsconfig.json +++ b/examples/client-quickstart/tsconfig.json @@ -10,11 +10,11 @@ "@modelcontextprotocol/client": ["./node_modules/@modelcontextprotocol/client/src/index.ts"], "@modelcontextprotocol/client/stdio": ["./node_modules/@modelcontextprotocol/client/src/stdio.ts"], "@modelcontextprotocol/client/_shims": ["./node_modules/@modelcontextprotocol/client/src/shimsNode.ts"], - "@modelcontextprotocol/core": [ - "./node_modules/@modelcontextprotocol/client/node_modules/@modelcontextprotocol/core/src/index.ts" + "@modelcontextprotocol/core-internal": [ + "./node_modules/@modelcontextprotocol/client/node_modules/@modelcontextprotocol/core-internal/src/index.ts" ], - "@modelcontextprotocol/core/public": [ - "./node_modules/@modelcontextprotocol/client/node_modules/@modelcontextprotocol/core/src/exports/public/index.ts" + "@modelcontextprotocol/core-internal/public": [ + "./node_modules/@modelcontextprotocol/client/node_modules/@modelcontextprotocol/core-internal/src/exports/public/index.ts" ] } }, diff --git a/examples/client/tsconfig.json b/examples/client/tsconfig.json index 5c1f7fc764..fa6b75f806 100644 --- a/examples/client/tsconfig.json +++ b/examples/client/tsconfig.json @@ -8,11 +8,11 @@ "@modelcontextprotocol/client": ["./node_modules/@modelcontextprotocol/client/src/index.ts"], "@modelcontextprotocol/client/stdio": ["./node_modules/@modelcontextprotocol/client/src/stdio.ts"], "@modelcontextprotocol/client/_shims": ["./node_modules/@modelcontextprotocol/client/src/shimsNode.ts"], - "@modelcontextprotocol/core": [ - "./node_modules/@modelcontextprotocol/client/node_modules/@modelcontextprotocol/core/src/index.ts" + "@modelcontextprotocol/core-internal": [ + "./node_modules/@modelcontextprotocol/client/node_modules/@modelcontextprotocol/core-internal/src/index.ts" ], - "@modelcontextprotocol/core/public": [ - "./node_modules/@modelcontextprotocol/client/node_modules/@modelcontextprotocol/core/src/exports/public/index.ts" + "@modelcontextprotocol/core-internal/public": [ + "./node_modules/@modelcontextprotocol/client/node_modules/@modelcontextprotocol/core-internal/src/exports/public/index.ts" ], "@modelcontextprotocol/eslint-config": ["./node_modules/@modelcontextprotocol/eslint-config/tsconfig.json"], "@modelcontextprotocol/vitest-config": ["./node_modules/@modelcontextprotocol/vitest-config/tsconfig.json"], diff --git a/examples/server-quickstart/tsconfig.json b/examples/server-quickstart/tsconfig.json index bd80e544cd..36dccebc19 100644 --- a/examples/server-quickstart/tsconfig.json +++ b/examples/server-quickstart/tsconfig.json @@ -10,11 +10,11 @@ "@modelcontextprotocol/server": ["./node_modules/@modelcontextprotocol/server/src/index.ts"], "@modelcontextprotocol/server/stdio": ["./node_modules/@modelcontextprotocol/server/src/stdio.ts"], "@modelcontextprotocol/server/_shims": ["./node_modules/@modelcontextprotocol/server/src/shimsNode.ts"], - "@modelcontextprotocol/core": [ - "./node_modules/@modelcontextprotocol/server/node_modules/@modelcontextprotocol/core/src/index.ts" + "@modelcontextprotocol/core-internal": [ + "./node_modules/@modelcontextprotocol/server/node_modules/@modelcontextprotocol/core-internal/src/index.ts" ], - "@modelcontextprotocol/core/public": [ - "./node_modules/@modelcontextprotocol/server/node_modules/@modelcontextprotocol/core/src/exports/public/index.ts" + "@modelcontextprotocol/core-internal/public": [ + "./node_modules/@modelcontextprotocol/server/node_modules/@modelcontextprotocol/core-internal/src/exports/public/index.ts" ] } }, diff --git a/examples/server/tsconfig.json b/examples/server/tsconfig.json index 37a3e874f7..3f08072b3c 100644 --- a/examples/server/tsconfig.json +++ b/examples/server/tsconfig.json @@ -11,11 +11,11 @@ "@modelcontextprotocol/express": ["./node_modules/@modelcontextprotocol/express/src/index.ts"], "@modelcontextprotocol/node": ["./node_modules/@modelcontextprotocol/node/src/index.ts"], "@modelcontextprotocol/hono": ["./node_modules/@modelcontextprotocol/hono/src/index.ts"], - "@modelcontextprotocol/core": [ - "./node_modules/@modelcontextprotocol/server/node_modules/@modelcontextprotocol/core/src/index.ts" + "@modelcontextprotocol/core-internal": [ + "./node_modules/@modelcontextprotocol/server/node_modules/@modelcontextprotocol/core-internal/src/index.ts" ], - "@modelcontextprotocol/core/public": [ - "./node_modules/@modelcontextprotocol/server/node_modules/@modelcontextprotocol/core/src/exports/public/index.ts" + "@modelcontextprotocol/core-internal/public": [ + "./node_modules/@modelcontextprotocol/server/node_modules/@modelcontextprotocol/core-internal/src/exports/public/index.ts" ], "@modelcontextprotocol/examples-shared": ["./node_modules/@modelcontextprotocol/examples-shared/src/index.ts"], "@modelcontextprotocol/eslint-config": ["./node_modules/@modelcontextprotocol/eslint-config/tsconfig.json"], diff --git a/examples/shared/package.json b/examples/shared/package.json index a55c3dd2e6..5522bd6c8d 100644 --- a/examples/shared/package.json +++ b/examples/shared/package.json @@ -29,7 +29,7 @@ "test:watch": "vitest" }, "dependencies": { - "@modelcontextprotocol/core": "workspace:^", + "@modelcontextprotocol/core-internal": "workspace:^", "@modelcontextprotocol/server": "workspace:^", "@modelcontextprotocol/express": "workspace:^", "better-auth": "^1.4.17", diff --git a/examples/shared/tsconfig.json b/examples/shared/tsconfig.json index bfc4eab524..ba240dda77 100644 --- a/examples/shared/tsconfig.json +++ b/examples/shared/tsconfig.json @@ -10,11 +10,11 @@ "@modelcontextprotocol/server": ["./node_modules/@modelcontextprotocol/server/src/index.ts"], "@modelcontextprotocol/server/_shims": ["./node_modules/@modelcontextprotocol/server/src/shimsNode.ts"], "@modelcontextprotocol/express": ["./node_modules/@modelcontextprotocol/express/src/index.ts"], - "@modelcontextprotocol/core": [ - "./node_modules/@modelcontextprotocol/server/node_modules/@modelcontextprotocol/core/src/index.ts" + "@modelcontextprotocol/core-internal": [ + "./node_modules/@modelcontextprotocol/server/node_modules/@modelcontextprotocol/core-internal/src/index.ts" ], - "@modelcontextprotocol/core/public": [ - "./node_modules/@modelcontextprotocol/server/node_modules/@modelcontextprotocol/core/src/exports/public/index.ts" + "@modelcontextprotocol/core-internal/public": [ + "./node_modules/@modelcontextprotocol/server/node_modules/@modelcontextprotocol/core-internal/src/exports/public/index.ts" ], "@modelcontextprotocol/eslint-config": ["./node_modules/@modelcontextprotocol/eslint-config/tsconfig.json"], "@modelcontextprotocol/vitest-config": ["./node_modules/@modelcontextprotocol/vitest-config/tsconfig.json"], diff --git a/packages/client/CHANGELOG.md b/packages/client/CHANGELOG.md index bc024d94f3..1fa83764fd 100644 --- a/packages/client/CHANGELOG.md +++ b/packages/client/CHANGELOG.md @@ -61,7 +61,7 @@ For raw JSON Schema (e.g. TypeBox output), use the new `fromJsonSchema` adapter: ```typescript - import { fromJsonSchema, AjvJsonSchemaValidator } from '@modelcontextprotocol/core'; + import { fromJsonSchema, AjvJsonSchemaValidator } from '@modelcontextprotocol/core-internal'; server.registerTool( 'greet', @@ -74,7 +74,8 @@ **Breaking changes:** - `experimental.tasks.getTaskResult()` no longer accepts a `resultSchema` parameter. Returns `GetTaskPayloadResult` (a loose `Result`); cast to the expected type at the call site. - - Removed unused exports from `@modelcontextprotocol/core`: `SchemaInput`, `schemaToJson`, `parseSchemaAsync`, `getSchemaShape`, `getSchemaDescription`, `isOptionalSchema`, `unwrapOptionalSchema`. Use the new `standardSchemaToJsonSchema` and `validateStandardSchema` instead. + - Removed unused exports from `@modelcontextprotocol/core-internal`: `SchemaInput`, `schemaToJson`, `parseSchemaAsync`, `getSchemaShape`, `getSchemaDescription`, `isOptionalSchema`, `unwrapOptionalSchema`. Use the new `standardSchemaToJsonSchema` and `validateStandardSchema` + instead. - `completable()` remains Zod-specific (it relies on Zod's `.shape` introspection). - [#1710](https://github.com/modelcontextprotocol/typescript-sdk/pull/1710) [`e563e63`](https://github.com/modelcontextprotocol/typescript-sdk/commit/e563e63bd2b3c2c1d1137406bef3f842c946201e) Thanks [@felixweinberger](https://github.com/felixweinberger)! - Add `AuthProvider` for diff --git a/packages/client/eslint.config.mjs b/packages/client/eslint.config.mjs index 4f034f2235..dd9e88588a 100644 --- a/packages/client/eslint.config.mjs +++ b/packages/client/eslint.config.mjs @@ -6,7 +6,7 @@ export default [ ...baseConfig, { settings: { - 'import/internal-regex': '^@modelcontextprotocol/core' + 'import/internal-regex': '^@modelcontextprotocol/core-internal' } } ]; diff --git a/packages/client/package.json b/packages/client/package.json index c712172139..a200718e01 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -92,7 +92,7 @@ "zod": "catalog:runtimeShared" }, "devDependencies": { - "@modelcontextprotocol/core": "workspace:^", + "@modelcontextprotocol/core-internal": "workspace:^", "@modelcontextprotocol/tsconfig": "workspace:^", "@modelcontextprotocol/vitest-config": "workspace:^", "@modelcontextprotocol/eslint-config": "workspace:^", diff --git a/packages/client/src/client/auth.examples.ts b/packages/client/src/client/auth.examples.ts index 01531c9780..fc4c52deaf 100644 --- a/packages/client/src/client/auth.examples.ts +++ b/packages/client/src/client/auth.examples.ts @@ -7,7 +7,7 @@ * @module */ -import type { AuthorizationServerMetadata } from '@modelcontextprotocol/core'; +import type { AuthorizationServerMetadata } from '@modelcontextprotocol/core-internal'; import type { OAuthClientProvider } from './auth'; import { fetchToken } from './auth'; diff --git a/packages/client/src/client/auth.ts b/packages/client/src/client/auth.ts index 5f55fb7a08..9e47a38203 100644 --- a/packages/client/src/client/auth.ts +++ b/packages/client/src/client/auth.ts @@ -9,7 +9,7 @@ import type { OAuthMetadata, OAuthProtectedResourceMetadata, OAuthTokens -} from '@modelcontextprotocol/core'; +} from '@modelcontextprotocol/core-internal'; import { checkResourceAllowed, LATEST_PROTOCOL_VERSION, @@ -22,7 +22,7 @@ import { OAuthTokensSchema, OpenIdProviderDiscoveryMetadataSchema, resourceUrlFromServerUrl -} from '@modelcontextprotocol/core'; +} from '@modelcontextprotocol/core-internal'; import pkceChallenge from 'pkce-challenge'; /** diff --git a/packages/client/src/client/authExtensions.ts b/packages/client/src/client/authExtensions.ts index c62d635138..857118410c 100644 --- a/packages/client/src/client/authExtensions.ts +++ b/packages/client/src/client/authExtensions.ts @@ -5,7 +5,7 @@ * for common machine-to-machine authentication scenarios. */ -import type { FetchLike, OAuthClientInformation, OAuthClientMetadata, OAuthTokens } from '@modelcontextprotocol/core'; +import type { FetchLike, OAuthClientInformation, OAuthClientMetadata, OAuthTokens } from '@modelcontextprotocol/core-internal'; import type { CryptoKey, JWK } from 'jose'; import type { AddClientAuthentication, OAuthClientProvider } from './auth'; diff --git a/packages/client/src/client/client.examples.ts b/packages/client/src/client/client.examples.ts index e748463574..a02f7dcb3d 100644 --- a/packages/client/src/client/client.examples.ts +++ b/packages/client/src/client/client.examples.ts @@ -7,7 +7,7 @@ * @module */ -import type { Prompt, Resource, Tool } from '@modelcontextprotocol/core'; +import type { Prompt, Resource, Tool } from '@modelcontextprotocol/core-internal'; import { Client } from './client'; import { SSEClientTransport } from './sse'; diff --git a/packages/client/src/client/client.ts b/packages/client/src/client/client.ts index bc3a91150b..8277225d79 100644 --- a/packages/client/src/client/client.ts +++ b/packages/client/src/client/client.ts @@ -32,7 +32,7 @@ import type { Tool, Transport, UnsubscribeRequest -} from '@modelcontextprotocol/core'; +} from '@modelcontextprotocol/core-internal'; import { CallToolResultSchema, CompleteResultSchema, @@ -58,7 +58,7 @@ import { ReadResourceResultSchema, SdkError, SdkErrorCode -} from '@modelcontextprotocol/core'; +} from '@modelcontextprotocol/core-internal'; /** * Elicitation default application helper. Applies defaults to the `data` based on the `schema`. diff --git a/packages/client/src/client/crossAppAccess.ts b/packages/client/src/client/crossAppAccess.ts index 7db9371cae..2783f2002a 100644 --- a/packages/client/src/client/crossAppAccess.ts +++ b/packages/client/src/client/crossAppAccess.ts @@ -8,8 +8,8 @@ * @module */ -import type { FetchLike } from '@modelcontextprotocol/core'; -import { IdJagTokenExchangeResponseSchema, OAuthErrorResponseSchema, OAuthTokensSchema } from '@modelcontextprotocol/core'; +import type { FetchLike } from '@modelcontextprotocol/core-internal'; +import { IdJagTokenExchangeResponseSchema, OAuthErrorResponseSchema, OAuthTokensSchema } from '@modelcontextprotocol/core-internal'; import type { ClientAuthMethod } from './auth'; import { applyClientAuthentication, discoverAuthorizationServerMetadata } from './auth'; diff --git a/packages/client/src/client/middleware.ts b/packages/client/src/client/middleware.ts index f5f36ae215..c86db72d2c 100644 --- a/packages/client/src/client/middleware.ts +++ b/packages/client/src/client/middleware.ts @@ -1,4 +1,4 @@ -import type { FetchLike } from '@modelcontextprotocol/core'; +import type { FetchLike } from '@modelcontextprotocol/core-internal'; import type { OAuthClientProvider } from './auth'; import { auth, extractWWWAuthenticateParams, UnauthorizedError } from './auth'; diff --git a/packages/client/src/client/sse.ts b/packages/client/src/client/sse.ts index fcd93f0594..3038ebfd82 100644 --- a/packages/client/src/client/sse.ts +++ b/packages/client/src/client/sse.ts @@ -1,4 +1,4 @@ -import type { FetchLike, JSONRPCMessage, Transport } from '@modelcontextprotocol/core'; +import type { FetchLike, JSONRPCMessage, Transport } from '@modelcontextprotocol/core-internal'; import { createFetchWithInit, JSONRPCMessageSchema, @@ -6,7 +6,7 @@ import { SdkError, SdkErrorCode, SdkHttpError -} from '@modelcontextprotocol/core'; +} from '@modelcontextprotocol/core-internal'; import type { ErrorEvent, EventSourceInit } from 'eventsource'; import { EventSource } from 'eventsource'; diff --git a/packages/client/src/client/stdio.ts b/packages/client/src/client/stdio.ts index 37c6d9a252..29b0027eae 100644 --- a/packages/client/src/client/stdio.ts +++ b/packages/client/src/client/stdio.ts @@ -3,8 +3,8 @@ import process from 'node:process'; import type { Stream } from 'node:stream'; import { PassThrough } from 'node:stream'; -import type { JSONRPCMessage, Transport } from '@modelcontextprotocol/core'; -import { ReadBuffer, SdkError, SdkErrorCode, serializeMessage } from '@modelcontextprotocol/core'; +import type { JSONRPCMessage, Transport } from '@modelcontextprotocol/core-internal'; +import { ReadBuffer, SdkError, SdkErrorCode, serializeMessage } from '@modelcontextprotocol/core-internal'; import spawn from 'cross-spawn'; export type StdioServerParameters = { diff --git a/packages/client/src/client/streamableHttp.ts b/packages/client/src/client/streamableHttp.ts index 7a7da69dc1..8962cf5639 100644 --- a/packages/client/src/client/streamableHttp.ts +++ b/packages/client/src/client/streamableHttp.ts @@ -1,6 +1,6 @@ import type { ReadableWritablePair } from 'node:stream/web'; -import type { FetchLike, JSONRPCMessage, Transport } from '@modelcontextprotocol/core'; +import type { FetchLike, JSONRPCMessage, Transport } from '@modelcontextprotocol/core-internal'; import { createFetchWithInit, isInitializedNotification, @@ -12,7 +12,7 @@ import { SdkError, SdkErrorCode, SdkHttpError -} from '@modelcontextprotocol/core'; +} from '@modelcontextprotocol/core-internal'; import { EventSourceParserStream } from 'eventsource-parser/stream'; import type { AuthProvider, OAuthClientProvider } from './auth'; diff --git a/packages/client/src/fromJsonSchema.ts b/packages/client/src/fromJsonSchema.ts index 575db2a8c4..588a096d5f 100644 --- a/packages/client/src/fromJsonSchema.ts +++ b/packages/client/src/fromJsonSchema.ts @@ -1,6 +1,6 @@ import { DefaultJsonSchemaValidator } from '@modelcontextprotocol/client/_shims'; -import type { JsonSchemaType, jsonSchemaValidator, StandardSchemaWithJSON } from '@modelcontextprotocol/core'; -import { fromJsonSchema as coreFromJsonSchema } from '@modelcontextprotocol/core'; +import type { JsonSchemaType, jsonSchemaValidator, StandardSchemaWithJSON } from '@modelcontextprotocol/core-internal'; +import { fromJsonSchema as coreFromJsonSchema } from '@modelcontextprotocol/core-internal'; let _defaultValidator: jsonSchemaValidator | undefined; diff --git a/packages/client/src/index.ts b/packages/client/src/index.ts index 69cee24488..b48d7cd0e8 100644 --- a/packages/client/src/index.ts +++ b/packages/client/src/index.ts @@ -2,7 +2,7 @@ // // This file defines the complete public surface. It consists of: // - Package-specific exports: listed explicitly below (named imports) -// - Protocol-level types: re-exported from @modelcontextprotocol/core/public +// - Protocol-level types: re-exported from @modelcontextprotocol/core-internal/public // // Any new export added here becomes public API. Use named exports, not wildcards. @@ -75,4 +75,4 @@ export { StreamableHTTPClientTransport } from './client/streamableHttp'; export { fromJsonSchema } from './fromJsonSchema'; // re-export curated public API from core -export * from '@modelcontextprotocol/core/public'; +export * from '@modelcontextprotocol/core-internal/public'; diff --git a/packages/client/src/shimsBrowser.ts b/packages/client/src/shimsBrowser.ts index de126d53bf..ee5d6373f9 100644 --- a/packages/client/src/shimsBrowser.ts +++ b/packages/client/src/shimsBrowser.ts @@ -3,7 +3,7 @@ * * This file is selected via package.json export conditions when running in a browser. */ -export { CfWorkerJsonSchemaValidator as DefaultJsonSchemaValidator } from '@modelcontextprotocol/core/validators/cfWorker'; +export { CfWorkerJsonSchemaValidator as DefaultJsonSchemaValidator } from '@modelcontextprotocol/core-internal/validators/cfWorker'; /** * Whether `fetch()` may throw `TypeError` due to CORS. Only true in browser contexts diff --git a/packages/client/src/shimsNode.ts b/packages/client/src/shimsNode.ts index de48ea2de6..5c411d731b 100644 --- a/packages/client/src/shimsNode.ts +++ b/packages/client/src/shimsNode.ts @@ -3,7 +3,7 @@ * * This file is selected via package.json export conditions when running in Node.js. */ -export { AjvJsonSchemaValidator as DefaultJsonSchemaValidator } from '@modelcontextprotocol/core/validators/ajv'; +export { AjvJsonSchemaValidator as DefaultJsonSchemaValidator } from '@modelcontextprotocol/core-internal/validators/ajv'; /** * Whether `fetch()` may throw `TypeError` due to CORS. CORS is a browser-only concept — diff --git a/packages/client/src/shimsWorkerd.ts b/packages/client/src/shimsWorkerd.ts index 9e6660b9ab..bd680e4358 100644 --- a/packages/client/src/shimsWorkerd.ts +++ b/packages/client/src/shimsWorkerd.ts @@ -3,7 +3,7 @@ * * This file is selected via package.json export conditions when running in workerd. */ -export { CfWorkerJsonSchemaValidator as DefaultJsonSchemaValidator } from '@modelcontextprotocol/core/validators/cfWorker'; +export { CfWorkerJsonSchemaValidator as DefaultJsonSchemaValidator } from '@modelcontextprotocol/core-internal/validators/cfWorker'; /** * Whether `fetch()` may throw `TypeError` due to CORS. CORS is a browser-only concept — diff --git a/packages/client/src/validators/ajv.ts b/packages/client/src/validators/ajv.ts index 770df3f57a..059c6b73f2 100644 --- a/packages/client/src/validators/ajv.ts +++ b/packages/client/src/validators/ajv.ts @@ -11,4 +11,4 @@ * const validator = new AjvJsonSchemaValidator(ajv); * ``` */ -export { addFormats, Ajv, AjvJsonSchemaValidator } from '@modelcontextprotocol/core/validators/ajv'; +export { addFormats, Ajv, AjvJsonSchemaValidator } from '@modelcontextprotocol/core-internal/validators/ajv'; diff --git a/packages/client/src/validators/cfWorker.ts b/packages/client/src/validators/cfWorker.ts index 2969b4dc9d..f1d9379afc 100644 --- a/packages/client/src/validators/cfWorker.ts +++ b/packages/client/src/validators/cfWorker.ts @@ -1,3 +1,3 @@ /** Customisation entry point for the `@cfworker/json-schema` validator. */ -export type { CfWorkerSchemaDraft } from '@modelcontextprotocol/core/validators/cfWorker'; -export { CfWorkerJsonSchemaValidator } from '@modelcontextprotocol/core/validators/cfWorker'; +export type { CfWorkerSchemaDraft } from '@modelcontextprotocol/core-internal/validators/cfWorker'; +export { CfWorkerJsonSchemaValidator } from '@modelcontextprotocol/core-internal/validators/cfWorker'; diff --git a/packages/client/test/client/auth.test.ts b/packages/client/test/client/auth.test.ts index 8239024c0c..102ab4264a 100644 --- a/packages/client/test/client/auth.test.ts +++ b/packages/client/test/client/auth.test.ts @@ -1,5 +1,5 @@ -import type { AuthorizationServerMetadata, OAuthClientMetadata, OAuthTokens } from '@modelcontextprotocol/core'; -import { LATEST_PROTOCOL_VERSION, OAuthError, OAuthErrorCode } from '@modelcontextprotocol/core'; +import type { AuthorizationServerMetadata, OAuthClientMetadata, OAuthTokens } from '@modelcontextprotocol/core-internal'; +import { LATEST_PROTOCOL_VERSION, OAuthError, OAuthErrorCode } from '@modelcontextprotocol/core-internal'; import type { Mock } from 'vitest'; import { expect, vi } from 'vitest'; @@ -3425,7 +3425,7 @@ describe('OAuth Authorization', () => { describe('RequestInit headers passthrough', () => { it('custom headers from RequestInit are passed to auth discovery requests', async () => { - const { createFetchWithInit } = await import('@modelcontextprotocol/core'); + const { createFetchWithInit } = await import('@modelcontextprotocol/core-internal'); const customFetch = vi.fn().mockResolvedValue({ ok: true, @@ -3458,7 +3458,7 @@ describe('OAuth Authorization', () => { }); it('auth-specific headers override base headers from RequestInit', async () => { - const { createFetchWithInit } = await import('@modelcontextprotocol/core'); + const { createFetchWithInit } = await import('@modelcontextprotocol/core-internal'); const customFetch = vi.fn().mockResolvedValue({ ok: true, @@ -3496,7 +3496,7 @@ describe('OAuth Authorization', () => { }); it('other RequestInit options are passed through', async () => { - const { createFetchWithInit } = await import('@modelcontextprotocol/core'); + const { createFetchWithInit } = await import('@modelcontextprotocol/core-internal'); const customFetch = vi.fn().mockResolvedValue({ ok: true, diff --git a/packages/client/test/client/crossAppAccess.test.ts b/packages/client/test/client/crossAppAccess.test.ts index 166f08fa9b..f403bf80a2 100644 --- a/packages/client/test/client/crossAppAccess.test.ts +++ b/packages/client/test/client/crossAppAccess.test.ts @@ -1,4 +1,4 @@ -import type { FetchLike } from '@modelcontextprotocol/core'; +import type { FetchLike } from '@modelcontextprotocol/core-internal'; import { describe, expect, it, vi } from 'vitest'; import { discoverAndRequestJwtAuthGrant, exchangeJwtAuthGrant, requestJwtAuthorizationGrant } from '../../src/client/crossAppAccess'; diff --git a/packages/client/test/client/crossSpawn.test.ts b/packages/client/test/client/crossSpawn.test.ts index f7e8823091..41839565ab 100644 --- a/packages/client/test/client/crossSpawn.test.ts +++ b/packages/client/test/client/crossSpawn.test.ts @@ -1,6 +1,6 @@ import type { ChildProcess } from 'node:child_process'; -import type { JSONRPCMessage } from '@modelcontextprotocol/core'; +import type { JSONRPCMessage } from '@modelcontextprotocol/core-internal'; import spawn from 'cross-spawn'; import type { Mock, MockedFunction } from 'vitest'; diff --git a/packages/client/test/client/jsonSchemaValidatorOverride.test.ts b/packages/client/test/client/jsonSchemaValidatorOverride.test.ts index f61a66033e..82c13a7021 100644 --- a/packages/client/test/client/jsonSchemaValidatorOverride.test.ts +++ b/packages/client/test/client/jsonSchemaValidatorOverride.test.ts @@ -1,5 +1,5 @@ -import type { JSONRPCMessage, JsonSchemaType, JsonSchemaValidatorResult, jsonSchemaValidator } from '@modelcontextprotocol/core'; -import { InMemoryTransport, LATEST_PROTOCOL_VERSION } from '@modelcontextprotocol/core'; +import type { JSONRPCMessage, JsonSchemaType, JsonSchemaValidatorResult, jsonSchemaValidator } from '@modelcontextprotocol/core-internal'; +import { InMemoryTransport, LATEST_PROTOCOL_VERSION } from '@modelcontextprotocol/core-internal'; import { Client } from '../../src/client/client'; import { fromJsonSchema } from '../../src/fromJsonSchema'; diff --git a/packages/client/test/client/middleware.test.ts b/packages/client/test/client/middleware.test.ts index f4090b4ed0..c0c0886952 100644 --- a/packages/client/test/client/middleware.test.ts +++ b/packages/client/test/client/middleware.test.ts @@ -1,4 +1,4 @@ -import type { FetchLike } from '@modelcontextprotocol/core'; +import type { FetchLike } from '@modelcontextprotocol/core-internal'; import type { Mocked, MockedFunction, MockInstance } from 'vitest'; import type { OAuthClientProvider } from '../../src/client/auth'; diff --git a/packages/client/test/client/sse.test.ts b/packages/client/test/client/sse.test.ts index f44681e589..a5e79f6c99 100644 --- a/packages/client/test/client/sse.test.ts +++ b/packages/client/test/client/sse.test.ts @@ -2,8 +2,8 @@ import type { IncomingMessage, Server, ServerResponse } from 'node:http'; import { createServer } from 'node:http'; import type { AddressInfo } from 'node:net'; -import type { JSONRPCMessage, OAuthTokens } from '@modelcontextprotocol/core'; -import { OAuthError, OAuthErrorCode, SdkErrorCode, SdkHttpError } from '@modelcontextprotocol/core'; +import type { JSONRPCMessage, OAuthTokens } from '@modelcontextprotocol/core-internal'; +import { OAuthError, OAuthErrorCode, SdkErrorCode, SdkHttpError } from '@modelcontextprotocol/core-internal'; import { listenOnRandomPort } from '@modelcontextprotocol/test-helpers'; import type { Mock, Mocked, MockedFunction, MockInstance } from 'vitest'; diff --git a/packages/client/test/client/stdio.test.ts b/packages/client/test/client/stdio.test.ts index 6b13d7d819..594ad6dc63 100644 --- a/packages/client/test/client/stdio.test.ts +++ b/packages/client/test/client/stdio.test.ts @@ -1,4 +1,4 @@ -import type { JSONRPCMessage } from '@modelcontextprotocol/core'; +import type { JSONRPCMessage } from '@modelcontextprotocol/core-internal'; import type { StdioServerParameters } from '../../src/client/stdio'; import { StdioClientTransport } from '../../src/client/stdio'; diff --git a/packages/client/test/client/streamableHttp.test.ts b/packages/client/test/client/streamableHttp.test.ts index db1f80fe9e..80a3734616 100644 --- a/packages/client/test/client/streamableHttp.test.ts +++ b/packages/client/test/client/streamableHttp.test.ts @@ -1,5 +1,5 @@ -import type { JSONRPCMessage, JSONRPCRequest } from '@modelcontextprotocol/core'; -import { OAuthError, OAuthErrorCode, SdkErrorCode, SdkHttpError } from '@modelcontextprotocol/core'; +import type { JSONRPCMessage, JSONRPCRequest } from '@modelcontextprotocol/core-internal'; +import { OAuthError, OAuthErrorCode, SdkErrorCode, SdkHttpError } from '@modelcontextprotocol/core-internal'; import type { Mock, Mocked } from 'vitest'; import type { OAuthClientProvider } from '../../src/client/auth'; diff --git a/packages/client/test/client/tokenProvider.test.ts b/packages/client/test/client/tokenProvider.test.ts index 89c2c69041..71cf11ab58 100644 --- a/packages/client/test/client/tokenProvider.test.ts +++ b/packages/client/test/client/tokenProvider.test.ts @@ -1,8 +1,8 @@ import type { IncomingMessage, Server } from 'node:http'; import { createServer } from 'node:http'; -import type { JSONRPCMessage, OAuthClientInformation, OAuthClientMetadata, OAuthTokens } from '@modelcontextprotocol/core'; -import { SdkErrorCode, SdkHttpError } from '@modelcontextprotocol/core'; +import type { JSONRPCMessage, OAuthClientInformation, OAuthClientMetadata, OAuthTokens } from '@modelcontextprotocol/core-internal'; +import { SdkErrorCode, SdkHttpError } from '@modelcontextprotocol/core-internal'; import { listenOnRandomPort } from '@modelcontextprotocol/test-helpers'; import type { Mock } from 'vitest'; diff --git a/packages/client/tsconfig.json b/packages/client/tsconfig.json index 5f47efeceb..f351f4cb76 100644 --- a/packages/client/tsconfig.json +++ b/packages/client/tsconfig.json @@ -5,11 +5,15 @@ "compilerOptions": { "paths": { "*": ["./*"], - "@modelcontextprotocol/core": ["./node_modules/@modelcontextprotocol/core/src/index.ts"], - "@modelcontextprotocol/core/public": ["./node_modules/@modelcontextprotocol/core/src/exports/public/index.ts"], - "@modelcontextprotocol/core/validators/ajv": ["./node_modules/@modelcontextprotocol/core/src/validators/ajvProvider.ts"], - "@modelcontextprotocol/core/validators/cfWorker": [ - "./node_modules/@modelcontextprotocol/core/src/validators/cfWorkerProvider.ts" + "@modelcontextprotocol/core-internal": ["./node_modules/@modelcontextprotocol/core-internal/src/index.ts"], + "@modelcontextprotocol/core-internal/public": [ + "./node_modules/@modelcontextprotocol/core-internal/src/exports/public/index.ts" + ], + "@modelcontextprotocol/core-internal/validators/ajv": [ + "./node_modules/@modelcontextprotocol/core-internal/src/validators/ajvProvider.ts" + ], + "@modelcontextprotocol/core-internal/validators/cfWorker": [ + "./node_modules/@modelcontextprotocol/core-internal/src/validators/cfWorkerProvider.ts" ], "@modelcontextprotocol/test-helpers": ["./node_modules/@modelcontextprotocol/test-helpers/src/index.ts"], "@modelcontextprotocol/client/_shims": ["./src/shimsNode.ts"] diff --git a/packages/client/tsdown.config.ts b/packages/client/tsdown.config.ts index 773e07c920..883fbf6f40 100644 --- a/packages/client/tsdown.config.ts +++ b/packages/client/tsdown.config.ts @@ -24,13 +24,13 @@ export default defineConfig({ compilerOptions: { baseUrl: '.', paths: { - '@modelcontextprotocol/core': ['../core/src/index.ts'], - '@modelcontextprotocol/core/public': ['../core/src/exports/public/index.ts'], - '@modelcontextprotocol/core/validators/ajv': ['../core/src/validators/ajvProvider.ts'], - '@modelcontextprotocol/core/validators/cfWorker': ['../core/src/validators/cfWorkerProvider.ts'] + '@modelcontextprotocol/core-internal': ['../core-internal/src/index.ts'], + '@modelcontextprotocol/core-internal/public': ['../core-internal/src/exports/public/index.ts'], + '@modelcontextprotocol/core-internal/validators/ajv': ['../core-internal/src/validators/ajvProvider.ts'], + '@modelcontextprotocol/core-internal/validators/cfWorker': ['../core-internal/src/validators/cfWorkerProvider.ts'] } } }, - noExternal: ['@modelcontextprotocol/core', 'ajv', 'ajv-formats', '@cfworker/json-schema'], + noExternal: ['@modelcontextprotocol/core-internal', 'ajv', 'ajv-formats', '@cfworker/json-schema'], external: ['@modelcontextprotocol/client/_shims'] }); diff --git a/packages/codemod/scripts/generateVersions.ts b/packages/codemod/scripts/generateVersions.ts index fa37e2c273..f4d827d76d 100644 --- a/packages/codemod/scripts/generateVersions.ts +++ b/packages/codemod/scripts/generateVersions.ts @@ -11,7 +11,7 @@ const PACKAGE_DIRS: Record = { '@modelcontextprotocol/node': 'middleware/node', '@modelcontextprotocol/express': 'middleware/express', '@modelcontextprotocol/server-legacy': 'server-legacy', - '@modelcontextprotocol/sdk-shared': 'sdk-shared' + '@modelcontextprotocol/core': 'core' }; const versions: Record = {}; diff --git a/packages/codemod/src/bin/batchTest.ts b/packages/codemod/src/bin/batchTest.ts index e339531928..f935e90ca5 100644 --- a/packages/codemod/src/bin/batchTest.ts +++ b/packages/codemod/src/bin/batchTest.ts @@ -85,10 +85,10 @@ const BATCH_DIR = path.resolve(SDK_ROOT, 'packages/codemod/batch-test'); const LOCAL_PACKAGE_DIRS: Record = { '@modelcontextprotocol/client': path.join(SDK_ROOT, 'packages/client'), - '@modelcontextprotocol/core': path.join(SDK_ROOT, 'packages/core'), + '@modelcontextprotocol/core-internal': path.join(SDK_ROOT, 'packages/core-internal'), '@modelcontextprotocol/server': path.join(SDK_ROOT, 'packages/server'), '@modelcontextprotocol/server-legacy': path.join(SDK_ROOT, 'packages/server-legacy'), - '@modelcontextprotocol/sdk-shared': path.join(SDK_ROOT, 'packages/sdk-shared'), + '@modelcontextprotocol/core': path.join(SDK_ROOT, 'packages/core'), '@modelcontextprotocol/express': path.join(SDK_ROOT, 'packages/middleware/express'), '@modelcontextprotocol/fastify': path.join(SDK_ROOT, 'packages/middleware/fastify'), '@modelcontextprotocol/hono': path.join(SDK_ROOT, 'packages/middleware/hono'), diff --git a/packages/codemod/src/generated/versions.ts b/packages/codemod/src/generated/versions.ts index afcef6e1ed..4fa12a1a87 100644 --- a/packages/codemod/src/generated/versions.ts +++ b/packages/codemod/src/generated/versions.ts @@ -5,5 +5,5 @@ export const V2_PACKAGE_VERSIONS: Record = { '@modelcontextprotocol/node': '^2.0.0-alpha.2', '@modelcontextprotocol/express': '^2.0.0-alpha.2', '@modelcontextprotocol/server-legacy': '^2.0.0-alpha.2', - '@modelcontextprotocol/sdk-shared': '^2.0.0-alpha.0' + '@modelcontextprotocol/core': '^2.0.0-alpha.0' }; diff --git a/packages/codemod/src/migrations/v1-to-v2/mappings/authSchemaNames.ts b/packages/codemod/src/migrations/v1-to-v2/mappings/authSchemaNames.ts index 869d507bbb..89a9083b15 100644 --- a/packages/codemod/src/migrations/v1-to-v2/mappings/authSchemaNames.ts +++ b/packages/codemod/src/migrations/v1-to-v2/mappings/authSchemaNames.ts @@ -1,11 +1,11 @@ // The auth (OAuth / OpenID) Zod schema CONSTANTS that v1 exported from -// `@modelcontextprotocol/sdk/shared/auth.js` and that sdk-shared still re-exports in v2. The import -// transform routes a `*Schema` symbol imported from that v1 path to sdk-shared when its name is in this +// `@modelcontextprotocol/sdk/shared/auth.js` and that core still re-exports in v2. The import +// transform routes a `*Schema` symbol imported from that v1 path to core when its name is in this // set (the corresponding TYPES, e.g. OAuthTokens, resolve by context to @modelcontextprotocol/client | -// /server). This is the v1 auth-schema set — a SUBSET of sdk-shared's auth exports. v2-only auth schemas -// (e.g. IdJagTokenExchangeResponseSchema) are exported by sdk-shared but NOT listed here: v1 never had +// /server). This is the v1 auth-schema set — a SUBSET of core's auth exports. v2-only auth schemas +// (e.g. IdJagTokenExchangeResponseSchema) are exported by core but NOT listed here: v1 never had // them, so there is nothing to migrate. test/v1-to-v2/authSchemaNames.test.ts asserts every name here is -// exported by sdk-shared (so the rewritten import resolves). Keep alphabetized. +// exported by core (so the rewritten import resolves). Keep alphabetized. export const AUTH_SCHEMA_NAMES: ReadonlySet = new Set([ 'OAuthClientInformationFullSchema', 'OAuthClientInformationSchema', @@ -21,10 +21,10 @@ export const AUTH_SCHEMA_NAMES: ReadonlySet = new Set([ ]); // v1's `@modelcontextprotocol/sdk/shared/auth.js` also exported these as Zod schema CONSTANTS, but they -// are typeless internal URL field-validators (no public spec type), so v2's sdk-shared deliberately does -// NOT re-export them (see packages/sdk-shared/src/index.ts) and no other public v2 package exports them. +// are typeless internal URL field-validators (no public spec type), so v2's core deliberately does +// NOT re-export them (see packages/core/src/index.ts) and no other public v2 package exports them. // They therefore have no v2 home: routed by context they would produce a codemod-introduced "has no // exported member" error. importPaths emits an actionRequired diagnostic instead of silently breaking. // test/v1-to-v2/authSchemaNames.test.ts asserts these are NOT in AUTH_SCHEMA_NAMES (and so are not -// claimed to be sdk-shared exports). +// claimed to be core exports). export const AUTH_SCHEMA_NAMES_NO_V2_PUBLIC_EXPORT: ReadonlySet = new Set(['SafeUrlSchema', 'OptionalSafeUrlSchema']); diff --git a/packages/codemod/src/migrations/v1-to-v2/mappings/importMap.ts b/packages/codemod/src/migrations/v1-to-v2/mappings/importMap.ts index 54e040c1ae..b49f7eba38 100644 --- a/packages/codemod/src/migrations/v1-to-v2/mappings/importMap.ts +++ b/packages/codemod/src/migrations/v1-to-v2/mappings/importMap.ts @@ -6,9 +6,9 @@ export interface ImportMapping { symbolTargetOverrides?: Record; /** * Route an imported symbol to this package (instead of `target`) when its rename-resolved name is - * a Zod schema constant re-exported by sdk-shared — a member of `SPEC_SCHEMA_NAMES` (spec schemas, + * a Zod schema constant re-exported by core — a member of `SPEC_SCHEMA_NAMES` (spec schemas, * for `sdk/types.js`) or `AUTH_SCHEMA_NAMES` (OAuth/OpenID schemas, for `sdk/shared/auth.js`). The - * schemas now live in `@modelcontextprotocol/sdk-shared` (so `Schema.parse(...)` keeps + * schemas now live in `@modelcontextprotocol/core` (so `Schema.parse(...)` keeps * working), while the corresponding types/constants/guards resolve by context. Matching on * membership (not a `*Schema` suffix) keeps TYPES whose name ends in `Schema` — e.g. the * elicitation primitives `BooleanSchema`/`StringSchema`/`EnumSchema` — routed by context, where @@ -130,7 +130,7 @@ export const IMPORT_MAP: Record = { '@modelcontextprotocol/sdk/types.js': { target: 'RESOLVE_BY_CONTEXT', status: 'moved', - schemaSymbolTarget: '@modelcontextprotocol/sdk-shared', + schemaSymbolTarget: '@modelcontextprotocol/core', renamedSymbols: { ResourceTemplate: 'ResourceTemplateType' } @@ -150,11 +150,11 @@ export const IMPORT_MAP: Record = { '@modelcontextprotocol/sdk/shared/auth.js': { target: 'RESOLVE_BY_CONTEXT', status: 'moved', - // OAuth/OpenID Zod schema constants (AUTH_SCHEMA_NAMES) are re-exported by sdk-shared as a + // OAuth/OpenID Zod schema constants (AUTH_SCHEMA_NAMES) are re-exported by core as a // separate group, so route them there (keeping `OAuthTokensSchema.parse(...)` working). The // OAuth/OpenID TYPES (OAuthTokens, etc.) carry no `schemaSymbolTarget` match and resolve by // context to @modelcontextprotocol/client | /server. - schemaSymbolTarget: '@modelcontextprotocol/sdk-shared' + schemaSymbolTarget: '@modelcontextprotocol/core' }, '@modelcontextprotocol/sdk/shared/stdio.js': { target: 'RESOLVE_BY_CONTEXT', diff --git a/packages/codemod/src/migrations/v1-to-v2/mappings/schemaRouting.ts b/packages/codemod/src/migrations/v1-to-v2/mappings/schemaRouting.ts index d5b18a7431..7aead7c17a 100644 --- a/packages/codemod/src/migrations/v1-to-v2/mappings/schemaRouting.ts +++ b/packages/codemod/src/migrations/v1-to-v2/mappings/schemaRouting.ts @@ -12,7 +12,7 @@ export function resolveRenamedName(name: string, mapping: ImportMapping): string } /** - * True when `name` (after renames) is a Zod schema CONSTANT that sdk-shared re-exports — either a spec + * True when `name` (after renames) is a Zod schema CONSTANT that core re-exports — either a spec * schema (`SPEC_SCHEMA_NAMES`) or an OAuth/OpenID schema (`AUTH_SCHEMA_NAMES`). Membership (not a * `*Schema` suffix) is what keeps TYPES whose name ends in `Schema` — e.g. `BooleanSchema` — out. */ @@ -25,7 +25,7 @@ export function isSharedSchemaConst(name: string, mapping: ImportMapping): boole * The per-symbol target package for a symbol imported/re-exported/mocked from `mapping`'s module, or * `undefined` when the symbol should use the mapping's resolved `target`. Exact-name * `symbolTargetOverrides` win over `schemaSymbolTarget`, which routes a symbol to the shared-schemas - * package only when its rename-resolved name is a schema constant re-exported by sdk-shared (see + * package only when its rename-resolved name is a schema constant re-exported by core (see * `isSharedSchemaConst`). */ export function symbolTargetOverride(name: string, mapping: ImportMapping): string | undefined { diff --git a/packages/codemod/src/migrations/v1-to-v2/mappings/specSchemaNames.ts b/packages/codemod/src/migrations/v1-to-v2/mappings/specSchemaNames.ts index 3007eaea8d..55712252bc 100644 --- a/packages/codemod/src/migrations/v1-to-v2/mappings/specSchemaNames.ts +++ b/packages/codemod/src/migrations/v1-to-v2/mappings/specSchemaNames.ts @@ -1,7 +1,7 @@ -// AUTO-VERIFIED against @modelcontextprotocol/sdk-shared's public exports by +// AUTO-VERIFIED against @modelcontextprotocol/core's public exports by // test/v1-to-v2/specSchemaNames.test.ts (drift guard). These are the spec Zod schema CONSTANTS that -// sdk-shared re-exports as standalone values; the v1->v2 import transform routes a `*Schema` symbol -// imported from `@modelcontextprotocol/sdk/types.js` to sdk-shared ONLY when its (rename-resolved) +// core re-exports as standalone values; the v1->v2 import transform routes a `*Schema` symbol +// imported from `@modelcontextprotocol/sdk/types.js` to core ONLY when its (rename-resolved) // name is in this set. Names that merely END in `Schema` but are NOT here — e.g. the elicitation // primitive TYPES `BooleanSchema`/`StringSchema`/`EnumSchema` (whose Zod const is `SchemaSchema`) // — fall through to context resolution (@modelcontextprotocol/client | /server), where their TYPES diff --git a/packages/codemod/src/migrations/v1-to-v2/mappings/symbolMap.ts b/packages/codemod/src/migrations/v1-to-v2/mappings/symbolMap.ts index f397679913..d7220da548 100644 --- a/packages/codemod/src/migrations/v1-to-v2/mappings/symbolMap.ts +++ b/packages/codemod/src/migrations/v1-to-v2/mappings/symbolMap.ts @@ -9,7 +9,7 @@ export const SIMPLE_RENAMES: Record = { // responses, so a migrated `JSONRPCResponseSchema.parse(...)` would silently widen. Rename to the // result-only schema to preserve v1 behavior — mirroring the isJSONRPCResponse guard rename above. // (The TYPE JSONRPCResponse/JSONRPCResultResponse is not part of the public v2 surface, so only the - // schema constant — re-exported by sdk-shared — is renamed here.) + // schema constant — re-exported by core — is renamed here.) JSONRPCResponseSchema: 'JSONRPCResultResponseSchema', ResourceReference: 'ResourceTemplateReference', ResourceReferenceSchema: 'ResourceTemplateReferenceSchema' diff --git a/packages/codemod/src/migrations/v1-to-v2/transforms/importPaths.ts b/packages/codemod/src/migrations/v1-to-v2/transforms/importPaths.ts index ba89263645..89397901f8 100644 --- a/packages/codemod/src/migrations/v1-to-v2/transforms/importPaths.ts +++ b/packages/codemod/src/migrations/v1-to-v2/transforms/importPaths.ts @@ -114,9 +114,9 @@ export const importPathsTransform: Transform = { // actually routes to the context package. resolveTypesPackage's diagnostic sink emits a "could // not determine project type" warning (or, for a 'both' project, an info note), so resolving // eagerly would emit that note even for an import of nothing but `*Schema` constants — which - // routes entirely to sdk-shared and never uses the context package. A namespace or default + // routes entirely to core and never uses the context package. A namespace or default // binding always needs context; a named symbol needs it only when it has no per-symbol override - // (i.e. it is not a `*Schema` routed to sdk-shared). + // (i.e. it is not a `*Schema` routed to core). let targetPackage = mapping.target; if (targetPackage === 'RESOLVE_BY_CONTEXT') { const needsContext = @@ -142,9 +142,9 @@ export const importPathsTransform: Transform = { // A namespace import (`import * as ns from …`) cannot be split per-symbol — usages are // qualified (`ns.Foo`), so the whole binding moves to one package. Named imports (aliased or // not), including the named siblings of a default import, DO fall through to the per-symbol - // splitter below — so an all-`*Schema` import routes entirely to sdk-shared, a single aliased + // splitter below — so an all-`*Schema` import routes entirely to core, a single aliased // specifier no longer forces unrelated symbols into the wrong package, and a mixed - // `import sdk, { CallToolResultSchema }` routes the schema to sdk-shared while the default + // `import sdk, { CallToolResultSchema }` routes the schema to core while the default // binding (handled at the end of the per-symbol path) moves to the context package. if (namespaceImport) { const effectiveTarget = targetPackage; @@ -152,8 +152,8 @@ export const importPathsTransform: Transform = { // namespace can't be split), so flag them. if (mapping.schemaSymbolTarget) { const nsName = namespaceImport.getText(); - // Map each accessed v1 name to the v2 name sdk-shared actually exports — some are - // renamed (e.g. JSONRPCErrorSchema → JSONRPCErrorResponseSchema), and sdk-shared only + // Map each accessed v1 name to the v2 name core actually exports — some are + // renamed (e.g. JSONRPCErrorSchema → JSONRPCErrorResponseSchema), and core only // exports the v2 name. Dedupe by the accessed (v1) name. const schemaAccesses = [ ...new Map( @@ -216,7 +216,7 @@ export const importPathsTransform: Transform = { filePath, imp, `${name} was an internal URL field-validator in v1's ${specifier} with no public v2 equivalent ` + - `(it is not re-exported by @modelcontextprotocol/sdk-shared). Remove this import and inline the ` + + `(it is not re-exported by @modelcontextprotocol/core). Remove this import and inline the ` + `validation (e.g. validate the URL with the WHATWG \`URL\` constructor or your own Zod schema).` ) ); diff --git a/packages/codemod/src/migrations/v1-to-v2/transforms/mockPaths.ts b/packages/codemod/src/migrations/v1-to-v2/transforms/mockPaths.ts index c212d70640..78e77aad03 100644 --- a/packages/codemod/src/migrations/v1-to-v2/transforms/mockPaths.ts +++ b/packages/codemod/src/migrations/v1-to-v2/transforms/mockPaths.ts @@ -16,7 +16,7 @@ import { SIMPLE_RENAMES } from '../mappings/symbolMap'; * specifier is a single string and cannot be split, so a mix can only be flagged, not rewritten. * Returns `target: undefined` when no symbol carries a per-symbol override (the caller keeps the * mapping's resolved context/`target` package). Mirrors `symbolTargetOverride` routing used by the - * static import/export transform so e.g. a factory of only `*Schema` constants routes to sdk-shared. + * static import/export transform so e.g. a factory of only `*Schema` constants routes to core. */ function routeSymbols(symbols: string[], mapping: ImportMapping): { target?: string; mixed: boolean } { if (symbols.length === 0) return { mixed: false }; @@ -101,7 +101,7 @@ function resolveTarget( // Return the original mapping (not just `renamedSymbols`/`symbolTargetOverrides`) so per-symbol // routing can consult `schemaSymbolTarget` via the shared `symbolTargetOverride`/`routeSymbols`, - // matching how the static import transform routes `*Schema` constants to sdk-shared. + // matching how the static import transform routes `*Schema` constants to core. return { target, mapping }; } @@ -147,7 +147,7 @@ function rewriteMockCall( let effectiveTarget = resolved.target; if (args.length >= 2) { // Route the factory's mocked symbols the same way the static import transform would: a factory of - // only `*Schema` constants (from sdk/types.js or sdk/shared/auth.js) moves to sdk-shared; a factory + // only `*Schema` constants (from sdk/types.js or sdk/shared/auth.js) moves to core; a factory // of only `StreamableHTTPServerTransport` moves to @modelcontextprotocol/node. A single mock path // can't be split, so a mix of packages is flagged for manual migration. const { target: routedTarget, mixed } = routeSymbols(collectFactorySymbols(args[1]!), resolved.mapping); @@ -292,7 +292,7 @@ function rewriteDynamicImports( // Route the destructured bindings the same way the static import transform would: a destructuring // of only `*Schema` constants (e.g. `const { CallToolResultSchema } = await import('…/types.js')`) - // moves to sdk-shared, and `StreamableHTTPServerTransport` moves to @modelcontextprotocol/node. A + // moves to core, and `StreamableHTTPServerTransport` moves to @modelcontextprotocol/node. A // single import() specifier can't be split, so a mix of packages is flagged for manual migration. const parentExpr = node.getParent(); if (parentExpr && Node.isAwaitExpression(parentExpr)) { diff --git a/packages/codemod/src/utils/importUtils.ts b/packages/codemod/src/utils/importUtils.ts index 5ad92cd25b..ecf3670af5 100644 --- a/packages/codemod/src/utils/importUtils.ts +++ b/packages/codemod/src/utils/importUtils.ts @@ -6,8 +6,8 @@ const SDK_PREFIX = '@modelcontextprotocol/sdk'; const V2_PACKAGES = new Set([ '@modelcontextprotocol/client', '@modelcontextprotocol/server', + '@modelcontextprotocol/core-internal', '@modelcontextprotocol/core', - '@modelcontextprotocol/sdk-shared', '@modelcontextprotocol/node', '@modelcontextprotocol/express' ]); diff --git a/packages/codemod/src/utils/packageJsonUpdater.ts b/packages/codemod/src/utils/packageJsonUpdater.ts index 54a7b97624..1b3eb07f8a 100644 --- a/packages/codemod/src/utils/packageJsonUpdater.ts +++ b/packages/codemod/src/utils/packageJsonUpdater.ts @@ -5,7 +5,7 @@ import type { PackageJsonChange } from '../types'; import { findPackageJson } from './projectAnalyzer'; const V1_PACKAGE = '@modelcontextprotocol/sdk'; -const PRIVATE_PACKAGES = new Set(['@modelcontextprotocol/core']); +const PRIVATE_PACKAGES = new Set(['@modelcontextprotocol/core-internal']); function normalizeToRoot(pkg: string): string { const secondSlash = pkg.indexOf('/', pkg.indexOf('/') + 1); diff --git a/packages/codemod/test/integration.test.ts b/packages/codemod/test/integration.test.ts index 151556f01c..c662c68b21 100644 --- a/packages/codemod/test/integration.test.ts +++ b/packages/codemod/test/integration.test.ts @@ -379,11 +379,11 @@ describe('integration', () => { expect(pkgJson.dependencies['express']).toBe('^4.0.0'); }); - it('package.json: does not add sdk-shared when every schema import is rewritten away', () => { + it('package.json: does not add core when every schema import is rewritten away', () => { // The dominant v1 pattern: a `*Schema` constant used ONLY as a setRequestHandler first arg. - // importPaths routes it to @modelcontextprotocol/sdk-shared (recording the package), but + // importPaths routes it to @modelcontextprotocol/core (recording the package), but // handlerRegistration then rewrites the call to a method string and deletes the now-unused - // import. No sdk-shared import survives, so package.json must NOT gain a sdk-shared dependency. + // import. No core import survives, so package.json must NOT gain a core dependency. const dir = createTempDir(); writePkgJson(dir, { dependencies: { '@modelcontextprotocol/sdk': '^1.0.0' } }); writeFileSync( @@ -402,21 +402,21 @@ describe('integration', () => { // The schema usage was rewritten and its import deleted. const output = readFileSync(path.join(dir, 'server.ts'), 'utf8'); expect(output).toContain("setRequestHandler('tools/call'"); - expect(output).not.toContain('sdk-shared'); + expect(output).not.toContain('core'); - // So sdk-shared must not be added; the package actually imported (server) still is. + // So core must not be added; the package actually imported (server) still is. expect(result.packageJsonChanges).toBeDefined(); expect(result.packageJsonChanges!.added).toContain('@modelcontextprotocol/server'); - expect(result.packageJsonChanges!.added).not.toContain('@modelcontextprotocol/sdk-shared'); + expect(result.packageJsonChanges!.added).not.toContain('@modelcontextprotocol/core'); const pkgJson = JSON.parse(readFileSync(path.join(dir, 'package.json'), 'utf8')); - expect(pkgJson.dependencies['@modelcontextprotocol/sdk-shared']).toBeUndefined(); + expect(pkgJson.dependencies['@modelcontextprotocol/core']).toBeUndefined(); expect(pkgJson.dependencies['@modelcontextprotocol/server']).toBeDefined(); }); - it('package.json: still adds sdk-shared when a schema import survives as a value', () => { + it('package.json: still adds core when a schema import survives as a value', () => { // Guard against over-correcting: a schema used as a value (e.g. `.parse(...)`) keeps its import, - // so sdk-shared remains a real dependency and must still be added. + // so core remains a real dependency and must still be added. const dir = createTempDir(); writePkgJson(dir, { dependencies: { '@modelcontextprotocol/sdk': '^1.0.0' } }); writeFileSync( @@ -433,14 +433,14 @@ describe('integration', () => { const result = run(migration, { targetDir: dir }); const output = readFileSync(path.join(dir, 'lib.ts'), 'utf8'); - expect(output).toContain('@modelcontextprotocol/sdk-shared'); + expect(output).toContain('@modelcontextprotocol/core'); expect(output).toContain('CallToolResultSchema.parse'); expect(result.packageJsonChanges).toBeDefined(); - expect(result.packageJsonChanges!.added).toContain('@modelcontextprotocol/sdk-shared'); + expect(result.packageJsonChanges!.added).toContain('@modelcontextprotocol/core'); const pkgJson = JSON.parse(readFileSync(path.join(dir, 'package.json'), 'utf8')); - expect(pkgJson.dependencies['@modelcontextprotocol/sdk-shared']).toBeDefined(); + expect(pkgJson.dependencies['@modelcontextprotocol/core']).toBeDefined(); }); it('does not modify package.json in dry-run mode', () => { diff --git a/packages/codemod/test/packageJsonUpdater.test.ts b/packages/codemod/test/packageJsonUpdater.test.ts index 2c272b8642..9b1e993e2f 100644 --- a/packages/codemod/test/packageJsonUpdater.test.ts +++ b/packages/codemod/test/packageJsonUpdater.test.ts @@ -145,7 +145,7 @@ describe('updatePackageJson', () => { expect(deps['@modelcontextprotocol/server']).toBeUndefined(); }); - it('filters out @modelcontextprotocol/core (private package)', () => { + it('filters out @modelcontextprotocol/core-internal (private package)', () => { const dir = createTempDir(); writePkgJson(dir, { dependencies: { @@ -153,15 +153,15 @@ describe('updatePackageJson', () => { } }); - const result = updatePackageJson(dir, new Set(['@modelcontextprotocol/core', '@modelcontextprotocol/server']), false); + const result = updatePackageJson(dir, new Set(['@modelcontextprotocol/core-internal', '@modelcontextprotocol/server']), false); expect(result).toBeDefined(); - expect(result!.added).not.toContain('@modelcontextprotocol/core'); + expect(result!.added).not.toContain('@modelcontextprotocol/core-internal'); expect(result!.added).toContain('@modelcontextprotocol/server'); const pkg = readPkgJson(dir); const deps = pkg.dependencies as Record; - expect(deps['@modelcontextprotocol/core']).toBeUndefined(); + expect(deps['@modelcontextprotocol/core-internal']).toBeUndefined(); }); it('preserves 4-space indentation', () => { diff --git a/packages/codemod/test/v1-to-v2/authSchemaNames.test.ts b/packages/codemod/test/v1-to-v2/authSchemaNames.test.ts index 71ab402667..6e500d9b3f 100644 --- a/packages/codemod/test/v1-to-v2/authSchemaNames.test.ts +++ b/packages/codemod/test/v1-to-v2/authSchemaNames.test.ts @@ -6,15 +6,15 @@ import { describe, expect, it } from 'vitest'; import { AUTH_SCHEMA_NAMES, AUTH_SCHEMA_NAMES_NO_V2_PUBLIC_EXPORT } from '../../src/migrations/v1-to-v2/mappings/authSchemaNames.js'; describe('AUTH_SCHEMA_NAMES (codemod auth schema-routing allowlist)', () => { - it('routes only auth schemas that @modelcontextprotocol/sdk-shared exports (drift guard)', () => { - // The import transform routes a `*Schema` symbol from sdk/shared/auth.js to sdk-shared only when - // its name is in AUTH_SCHEMA_NAMES, so EVERY name here MUST be exported by sdk-shared — otherwise + it('routes only auth schemas that @modelcontextprotocol/core exports (drift guard)', () => { + // The import transform routes a `*Schema` symbol from sdk/shared/auth.js to core only when + // its name is in AUTH_SCHEMA_NAMES, so EVERY name here MUST be exported by core — otherwise // the rewritten import would have no exported member. AUTH_SCHEMA_NAMES is the v1 auth-schema set, - // a SUBSET of sdk-shared's auth exports: sdk-shared may export more (v2-only schemas such as + // a SUBSET of core's auth exports: core may export more (v2-only schemas such as // IdJagTokenExchangeResponseSchema) that v1 never had and the codemod never encounters. Read - // sdk-shared's barrel directly (the `export { … } from '…/core/auth'` block) so they cannot drift. - const src = readFileSync(fileURLToPath(new URL('../../../sdk-shared/src/index.ts', import.meta.url)), 'utf8'); - const closeIdx = src.indexOf("} from '@modelcontextprotocol/core/auth'"); + // core's barrel directly (the `export { … } from '…/core/auth'` block) so they cannot drift. + const src = readFileSync(fileURLToPath(new URL('../../../core/src/index.ts', import.meta.url)), 'utf8'); + const closeIdx = src.indexOf("} from '@modelcontextprotocol/core-internal/auth'"); const openIdx = src.lastIndexOf('export {', closeIdx); const block = src.slice(openIdx + 'export {'.length, closeIdx); const sdkSharedAuthExports = new Set([...block.matchAll(/\b(\w+Schema)\b/g)].map(m => m[1])); @@ -27,7 +27,7 @@ describe('AUTH_SCHEMA_NAMES (codemod auth schema-routing allowlist)', () => { it('keeps the no-v2-home auth schemas OUT of the routing allowlist', () => { // SafeUrlSchema/OptionalSafeUrlSchema have no public v2 export, so they must NOT be routed to - // sdk-shared (the import transform flags them instead). Guard the two sets stay disjoint. + // core (the import transform flags them instead). Guard the two sets stay disjoint. for (const name of AUTH_SCHEMA_NAMES_NO_V2_PUBLIC_EXPORT) { expect(AUTH_SCHEMA_NAMES.has(name)).toBe(false); } diff --git a/packages/codemod/test/v1-to-v2/specSchemaNames.test.ts b/packages/codemod/test/v1-to-v2/specSchemaNames.test.ts index bd677aa7ed..73ca0d0a66 100644 --- a/packages/codemod/test/v1-to-v2/specSchemaNames.test.ts +++ b/packages/codemod/test/v1-to-v2/specSchemaNames.test.ts @@ -6,13 +6,13 @@ import { describe, expect, it } from 'vitest'; import { SPEC_SCHEMA_NAMES } from '../../src/migrations/v1-to-v2/mappings/specSchemaNames.js'; describe('SPEC_SCHEMA_NAMES (codemod schema-routing allowlist)', () => { - it("matches @modelcontextprotocol/sdk-shared's exported schema set exactly (drift guard)", () => { - // The import transform routes a `*Schema` symbol from sdk/types.js to sdk-shared only when the - // symbol's (rename-resolved) name is in this set. It must therefore equal sdk-shared's actual + it("matches @modelcontextprotocol/core's exported schema set exactly (drift guard)", () => { + // The import transform routes a `*Schema` symbol from sdk/types.js to core only when the + // symbol's (rename-resolved) name is in this set. It must therefore equal core's actual // public exports: a name missing here would be misrouted to client/server (which export no Zod - // schema values), and a name here that sdk-shared does not export would produce a broken import. - // Read sdk-shared's barrel directly so the two cannot silently drift. - const src = readFileSync(fileURLToPath(new URL('../../../sdk-shared/src/index.ts', import.meta.url)), 'utf8'); + // schema values), and a name here that core does not export would produce a broken import. + // Read core's barrel directly so the two cannot silently drift. + const src = readFileSync(fileURLToPath(new URL('../../../core/src/index.ts', import.meta.url)), 'utf8'); const block = src.slice(src.indexOf('export {') + 'export {'.length, src.indexOf('} from')); const sdkSharedExports = [...new Set([...block.matchAll(/\b(\w+Schema)\b/g)].map(m => m[1]))].sort(); expect([...SPEC_SCHEMA_NAMES].sort()).toEqual(sdkSharedExports); diff --git a/packages/codemod/test/v1-to-v2/transforms/importPaths.test.ts b/packages/codemod/test/v1-to-v2/transforms/importPaths.test.ts index de00e2941a..cc33f75ba8 100644 --- a/packages/codemod/test/v1-to-v2/transforms/importPaths.test.ts +++ b/packages/codemod/test/v1-to-v2/transforms/importPaths.test.ts @@ -83,7 +83,7 @@ describe('import-paths transform', () => { const result = applyTransform(input, { projectType: 'both' }); expect(result).toContain(`from "@modelcontextprotocol/client"`); expect(result).toContain('CallToolResult'); - expect(result).not.toContain('@modelcontextprotocol/sdk-shared'); + expect(result).not.toContain('@modelcontextprotocol/core'); }); it('resolves a sdk/types.js TYPE import based on sibling server imports', () => { @@ -97,41 +97,41 @@ describe('import-paths transform', () => { expect(result).toContain('CallToolResult'); }); - it('routes *Schema imports from sdk/types.js to @modelcontextprotocol/sdk-shared', () => { + it('routes *Schema imports from sdk/types.js to @modelcontextprotocol/core', () => { const input = `import { CallToolResultSchema } from '@modelcontextprotocol/sdk/types.js';\n`; const result = applyTransform(input, { projectType: 'server' }); - expect(result).toContain(`from "@modelcontextprotocol/sdk-shared"`); + expect(result).toContain(`from "@modelcontextprotocol/core"`); expect(result).toContain('CallToolResultSchema'); expect(result).not.toContain('@modelcontextprotocol/sdk/types'); }); - it('routes schemas to sdk-shared regardless of client/server sibling context', () => { - // The only sibling is a client import, but the schema must still go to sdk-shared. + it('routes schemas to core regardless of client/server sibling context', () => { + // The only sibling is a client import, but the schema must still go to core. const input = [ `import { Client } from '@modelcontextprotocol/sdk/client/index.js';`, `import { ListToolsResultSchema } from '@modelcontextprotocol/sdk/types.js';`, '' ].join('\n'); const result = applyTransform(input, { projectType: 'both' }); - expect(result).toContain(`from "@modelcontextprotocol/sdk-shared"`); + expect(result).toContain(`from "@modelcontextprotocol/core"`); expect(result).toContain('ListToolsResultSchema'); }); - it('splits a mixed type + schema import: type resolves by context, schema to sdk-shared', () => { + it('splits a mixed type + schema import: type resolves by context, schema to core', () => { const input = [ `import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';`, `import { CallToolResult, CallToolResultSchema } from '@modelcontextprotocol/sdk/types.js';`, '' ].join('\n'); const result = applyTransform(input, { projectType: 'both' }); - expect(result).toContain(`from "@modelcontextprotocol/sdk-shared"`); + expect(result).toContain(`from "@modelcontextprotocol/core"`); expect(result).toContain(`from "@modelcontextprotocol/server"`); expect(result).toContain('CallToolResult'); expect(result).toContain('CallToolResultSchema'); expect(result).not.toContain('@modelcontextprotocol/sdk/types'); }); - it('splits an aliased types.js import: schema constant to sdk-shared, aliased type to server', () => { + it('splits an aliased types.js import: schema constant to core, aliased type to server', () => { // The presence of an alias (`Tool as SDKTool`) must not force the whole import into one package; // each symbol still routes to its correct v2 target, with the alias preserved. const input = [ @@ -139,7 +139,7 @@ describe('import-paths transform', () => { '' ].join('\n'); const result = applyTransform(input, { projectType: 'server' }); - expect(result).toMatch(/import\s*\{[^}]*\bCreateMessageRequestSchema\b[^}]*\}\s*from\s*["']@modelcontextprotocol\/sdk-shared["']/); + expect(result).toMatch(/import\s*\{[^}]*\bCreateMessageRequestSchema\b[^}]*\}\s*from\s*["']@modelcontextprotocol\/core["']/); expect(result).toMatch(/import\s*\{[^}]*\bClientCapabilities\b[^}]*\}\s*from\s*["']@modelcontextprotocol\/server["']/); expect(result).toContain('Tool as SDKTool'); // the schema constant must NOT end up imported from @modelcontextprotocol/server @@ -196,8 +196,8 @@ describe('import-paths transform', () => { expect(result).toContain('@modelcontextprotocol/server'); }); - it('routes OAuth *Schema from sdk/shared/auth.js to sdk-shared; the TYPE resolves by context', () => { - // OAuthTokensSchema is a Zod schema re-exported by sdk-shared (AUTH_SCHEMA_NAMES), so route it + it('routes OAuth *Schema from sdk/shared/auth.js to core; the TYPE resolves by context', () => { + // OAuthTokensSchema is a Zod schema re-exported by core (AUTH_SCHEMA_NAMES), so route it // there — `OAuthTokensSchema.parse(...)` keeps working. OAuthTokens (the type) has no schema-name // match and resolves by context to @modelcontextprotocol/client. const input = [ @@ -207,14 +207,14 @@ describe('import-paths transform', () => { '' ].join('\n'); const result = applyTransform(input, { projectType: 'client' }); - expect(result).toMatch(/import\s*\{[^}]*\bOAuthTokensSchema\b[^}]*\}\s*from\s*["']@modelcontextprotocol\/sdk-shared["']/); + expect(result).toMatch(/import\s*\{[^}]*\bOAuthTokensSchema\b[^}]*\}\s*from\s*["']@modelcontextprotocol\/core["']/); expect(result).toMatch(/import\s*\{[^}]*\bOAuthTokens\b[^}]*\}\s*from\s*["']@modelcontextprotocol\/client["']/); expect(result).toContain('OAuthTokensSchema.parse(raw)'); expect(result).not.toContain('@modelcontextprotocol/sdk/shared/auth'); }); - it('does not emit a project-type note when every symbol routes to sdk-shared (both project)', () => { - // A types.js import of nothing but `*Schema` constants routes entirely to sdk-shared, so the + it('does not emit a project-type note when every symbol routes to core (both project)', () => { + // A types.js import of nothing but `*Schema` constants routes entirely to core, so the // context package is never used — resolveTypesPackage must not be called, and no "both"-project // info note should be emitted. const project = new Project({ useInMemoryFileSystem: true }); @@ -224,11 +224,11 @@ describe('import-paths transform', () => { ); const result = importPathsTransform.apply(sourceFile, { projectType: 'both' }); expect(result.diagnostics.some(d => /both client and server|determine project type/i.test(d.message))).toBe(false); - expect(sourceFile.getFullText()).toContain('@modelcontextprotocol/sdk-shared'); + expect(sourceFile.getFullText()).toContain('@modelcontextprotocol/core'); expect(sourceFile.getFullText()).not.toContain('@modelcontextprotocol/sdk/types'); }); - it('does not warn about project type when an auth-schema-only import routes entirely to sdk-shared (unknown project)', () => { + it('does not warn about project type when an auth-schema-only import routes entirely to core (unknown project)', () => { const project = new Project({ useInMemoryFileSystem: true }); const sourceFile = project.createSourceFile( 'test.ts', @@ -236,7 +236,7 @@ describe('import-paths transform', () => { ); const result = importPathsTransform.apply(sourceFile, { projectType: 'unknown' }); expect(result.diagnostics.some(d => /determine project type/i.test(d.message))).toBe(false); - expect(sourceFile.getFullText()).toContain('@modelcontextprotocol/sdk-shared'); + expect(sourceFile.getFullText()).toContain('@modelcontextprotocol/core'); }); it('still warns about project type when a non-schema symbol falls through to context (unknown project)', () => { @@ -251,14 +251,14 @@ describe('import-paths transform', () => { expect(result.diagnostics.some(d => /determine project type/i.test(d.message))).toBe(true); }); - it('splits a mixed default + named schema import — schema to sdk-shared, default to context', () => { - // The named `CallToolResultSchema` must route to sdk-shared even though a default import is present; + it('splits a mixed default + named schema import — schema to core, default to context', () => { + // The named `CallToolResultSchema` must route to core even though a default import is present; // the default binding (which can't be split) moves to the context package. Pre-fix the whole import // moved to context and the schema silently became a "no exported member" error. const result = applyTransform(`import sdk, { CallToolResultSchema } from '@modelcontextprotocol/sdk/types.js';\n`, { projectType: 'server' }); - expect(result).toMatch(/import\s*\{[^}]*\bCallToolResultSchema\b[^}]*\}\s*from\s*["']@modelcontextprotocol\/sdk-shared["']/); + expect(result).toMatch(/import\s*\{[^}]*\bCallToolResultSchema\b[^}]*\}\s*from\s*["']@modelcontextprotocol\/core["']/); expect(result).toMatch(/import\s+sdk\s+from\s*["']@modelcontextprotocol\/server["']/); expect(result).not.toContain('@modelcontextprotocol/sdk/types'); }); @@ -271,12 +271,12 @@ describe('import-paths transform', () => { ].join('\n'); const result = applyTransform(input, { projectType: 'server' }); expect(result).toContain('CallToolResultSchema.parse(value)'); - expect(result).toContain(`from "@modelcontextprotocol/sdk-shared"`); + expect(result).toContain(`from "@modelcontextprotocol/core"`); }); - it('routes elicitation primitive *Schema TYPE names from sdk/types.js by context, not to sdk-shared', () => { + it('routes elicitation primitive *Schema TYPE names from sdk/types.js by context, not to core', () => { // These names END in `Schema` but are TYPES; their Zod constant is `SchemaSchema`. They - // must resolve to the context package (where the types live), never to sdk-shared (which only + // must resolve to the context package (where the types live), never to core (which only // exports the `*SchemaSchema` constants) — otherwise the codemod emits a broken import. const elicitationTypeNames = [ 'BooleanSchema', @@ -295,47 +295,47 @@ describe('import-paths transform', () => { const input = `import { ${typeName} } from '@modelcontextprotocol/sdk/types.js';\n`; const result = applyTransform(input, { projectType: 'server' }); expect(result, typeName).toContain(`from "@modelcontextprotocol/server"`); - expect(result, typeName).not.toContain('@modelcontextprotocol/sdk-shared'); + expect(result, typeName).not.toContain('@modelcontextprotocol/core'); expect(result, typeName).toContain(typeName); } }); it('splits a primitive-schema TYPE from its matching schema CONSTANT (BooleanSchema vs BooleanSchemaSchema)', () => { // They differ only by a trailing `Schema`, which the suffix heuristic could not distinguish. - // The constant goes to sdk-shared; the type resolves by context. + // The constant goes to core; the type resolves by context. const input = `import { BooleanSchema, BooleanSchemaSchema } from '@modelcontextprotocol/sdk/types.js';\n`; const result = applyTransform(input, { projectType: 'server' }); - expect(result).toContain(`from "@modelcontextprotocol/sdk-shared"`); + expect(result).toContain(`from "@modelcontextprotocol/core"`); expect(result).toContain('BooleanSchemaSchema'); expect(result).toContain(`from "@modelcontextprotocol/server"`); expect(result).toMatch(/BooleanSchema\b/); expect(result).not.toContain('@modelcontextprotocol/sdk/types'); }); - it('routes a renamed spec schema (JSONRPCErrorSchema) from sdk/types.js to sdk-shared', () => { - // JSONRPCErrorSchema → JSONRPCErrorResponseSchema, a sdk-shared export. Membership is checked + it('routes a renamed spec schema (JSONRPCErrorSchema) from sdk/types.js to core', () => { + // JSONRPCErrorSchema → JSONRPCErrorResponseSchema, a core export. Membership is checked // against the rename-resolved name; the symbolRenames transform applies the rename afterward, - // so importPaths alone leaves the name unchanged but routes it to sdk-shared. + // so importPaths alone leaves the name unchanged but routes it to core. const input = `import { JSONRPCErrorSchema } from '@modelcontextprotocol/sdk/types.js';\n`; const result = applyTransform(input, { projectType: 'server' }); - expect(result).toContain(`from "@modelcontextprotocol/sdk-shared"`); + expect(result).toContain(`from "@modelcontextprotocol/core"`); expect(result).toContain('JSONRPCErrorSchema'); expect(result).not.toContain('@modelcontextprotocol/sdk/types'); }); - it('routes JSONRPCResponseSchema (result-only in v1) from sdk/types.js to sdk-shared', () => { + it('routes JSONRPCResponseSchema (result-only in v1) from sdk/types.js to core', () => { // v1's JSONRPCResponseSchema validated only result responses; v2 reuses the name for a union. - // The rename to JSONRPCResultResponseSchema (a sdk-shared export) preserves v1 behavior; importPaths - // routes it to sdk-shared against the rename-resolved name (symbolRenames applies the rename after). + // The rename to JSONRPCResultResponseSchema (a core export) preserves v1 behavior; importPaths + // routes it to core against the rename-resolved name (symbolRenames applies the rename after). const input = `import { JSONRPCResponseSchema } from '@modelcontextprotocol/sdk/types.js';\n`; const result = applyTransform(input, { projectType: 'server' }); - expect(result).toContain(`from "@modelcontextprotocol/sdk-shared"`); + expect(result).toContain(`from "@modelcontextprotocol/core"`); expect(result).not.toContain('@modelcontextprotocol/sdk/types'); expect(result).not.toContain(`from "@modelcontextprotocol/server"`); }); it('flags a SafeUrlSchema import from sdk/shared/auth.js (no public v2 equivalent)', () => { - // SafeUrlSchema/OptionalSafeUrlSchema were internal URL field-validators in v1; v2's sdk-shared + // SafeUrlSchema/OptionalSafeUrlSchema were internal URL field-validators in v1; v2's core // deliberately does not re-export them, so there is no v2 home — emit guidance instead of silently // routing to a package that has no such export. const input = `import { SafeUrlSchema, OptionalSafeUrlSchema } from '@modelcontextprotocol/sdk/shared/auth.js';\n`; @@ -349,26 +349,26 @@ describe('import-paths transform', () => { it('flags a star re-export of sdk/types.js that drops the moved schema constants', () => { // `export * from '…/types.js'` cannot be routed per-symbol, so the Zod *Schema constants (now in - // sdk-shared) silently disappear from the re-exporting barrel. Surface that for the user. + // core) silently disappear from the re-exporting barrel. Surface that for the user. const input = `export * from '@modelcontextprotocol/sdk/types.js';\n`; const project = new Project({ useInMemoryFileSystem: true }); const sourceFile = project.createSourceFile('test.ts', input); const result = importPathsTransform.apply(sourceFile, { projectType: 'server' }); const messages = result.diagnostics.map(d => d.message).join('\n'); - expect(messages).toContain('@modelcontextprotocol/sdk-shared'); + expect(messages).toContain('@modelcontextprotocol/core'); expect(messages).toMatch(/Star re-export/i); }); - it('flags a star re-export of sdk/shared/auth.js (schema constants move to sdk-shared)', () => { + it('flags a star re-export of sdk/shared/auth.js (schema constants move to core)', () => { const input = `export * from '@modelcontextprotocol/sdk/shared/auth.js';\n`; const project = new Project({ useInMemoryFileSystem: true }); const sourceFile = project.createSourceFile('test.ts', input); const result = importPathsTransform.apply(sourceFile, { projectType: 'server' }); - expect(result.diagnostics.map(d => d.message).join('\n')).toContain('@modelcontextprotocol/sdk-shared'); + expect(result.diagnostics.map(d => d.message).join('\n')).toContain('@modelcontextprotocol/core'); }); it('emits a split diagnostic for a re-export mixing a spec schema and a *Schema type (no silent breakage)', () => { - // The `*Schema` suffix would have routed BooleanSchema to sdk-shared silently (no such export); + // The `*Schema` suffix would have routed BooleanSchema to core silently (no such export); // membership routing instead surfaces the mismatch so the user splits the re-export manually. const input = `export { CallToolResultSchema, BooleanSchema } from '@modelcontextprotocol/sdk/types.js';\n`; const project = new Project({ useInMemoryFileSystem: true }); @@ -388,7 +388,7 @@ describe('import-paths transform', () => { const result = importPathsTransform.apply(sourceFile, { projectType: 'server' }); const messages = result.diagnostics.map(d => d.message).join('\n'); // The namespace can't be split, so the schema can't be auto-routed — but the user must be told. - expect(messages).toContain('@modelcontextprotocol/sdk-shared'); + expect(messages).toContain('@modelcontextprotocol/core'); expect(messages).toContain('CallToolResultSchema'); // The namespace import itself still moves to the context package (its types live there). // (setModuleSpecifier preserves the original quote style, so match quote-agnostically.) @@ -396,7 +396,7 @@ describe('import-paths transform', () => { }); it('suggests the v2 (rename-resolved) name in the namespace schema-access diagnostic', () => { - // JSONRPCErrorSchema is re-exported by sdk-shared as JSONRPCErrorResponseSchema; the suggested + // JSONRPCErrorSchema is re-exported by core as JSONRPCErrorResponseSchema; the suggested // import must use the v2 name (the v1 name has no exported member), and mention the rename. const input = [ `import * as types from '@modelcontextprotocol/sdk/types.js';`, @@ -407,7 +407,7 @@ describe('import-paths transform', () => { const sourceFile = project.createSourceFile('test.ts', input); const result = importPathsTransform.apply(sourceFile, { projectType: 'server' }); const msg = result.diagnostics.map(d => d.message).join('\n'); - expect(msg).toContain("import { JSONRPCErrorResponseSchema } from '@modelcontextprotocol/sdk-shared'"); + expect(msg).toContain("import { JSONRPCErrorResponseSchema } from '@modelcontextprotocol/core'"); expect(msg).toContain('JSONRPCErrorSchema → JSONRPCErrorResponseSchema'); expect(msg).not.toContain('import { JSONRPCErrorSchema } from'); }); @@ -419,7 +419,7 @@ describe('import-paths transform', () => { const project = new Project({ useInMemoryFileSystem: true }); const sourceFile = project.createSourceFile('test.ts', input); const result = importPathsTransform.apply(sourceFile, { projectType: 'server' }); - expect(result.diagnostics.map(d => d.message).join('\n')).not.toContain('@modelcontextprotocol/sdk-shared'); + expect(result.diagnostics.map(d => d.message).join('\n')).not.toContain('@modelcontextprotocol/core'); }); it('resolves extensionless sdk/types (no .js suffix) the same as sdk/types.js', () => { diff --git a/packages/codemod/test/v1-to-v2/transforms/mockPaths.test.ts b/packages/codemod/test/v1-to-v2/transforms/mockPaths.test.ts index 4dd8d93bbf..40e8c577a2 100644 --- a/packages/codemod/test/v1-to-v2/transforms/mockPaths.test.ts +++ b/packages/codemod/test/v1-to-v2/transforms/mockPaths.test.ts @@ -339,18 +339,18 @@ describe('mock-paths transform', () => { }); describe('schema constant routing (schemaSymbolTarget)', () => { - it('routes a vi.mock factory of only spec *Schema constants to sdk-shared', () => { + it('routes a vi.mock factory of only spec *Schema constants to core', () => { const input = [`vi.mock('@modelcontextprotocol/sdk/types.js', () => ({`, ` CallToolResultSchema: vi.fn()`, `}));`, ''].join( '\n' ); const result = applyTransform(input); - expect(result).toContain(`'@modelcontextprotocol/sdk-shared'`); + expect(result).toContain(`'@modelcontextprotocol/core'`); expect(result).not.toContain('@modelcontextprotocol/sdk/types'); - // The schema constant lives in sdk-shared, never the context (server) package. + // The schema constant lives in core, never the context (server) package. expect(result).not.toContain(`'@modelcontextprotocol/server'`); }); - it('routes a vi.mock factory of only auth *Schema constants to sdk-shared', () => { + it('routes a vi.mock factory of only auth *Schema constants to core', () => { const input = [ `vi.mock('@modelcontextprotocol/sdk/shared/auth.js', () => ({`, ` OAuthTokensSchema: vi.fn()`, @@ -358,23 +358,23 @@ describe('mock-paths transform', () => { '' ].join('\n'); const result = applyTransform(input); - expect(result).toContain(`'@modelcontextprotocol/sdk-shared'`); + expect(result).toContain(`'@modelcontextprotocol/core'`); expect(result).not.toContain('@modelcontextprotocol/sdk/shared/auth'); }); - it('routes a destructured dynamic import of only *Schema constants to sdk-shared', () => { + it('routes a destructured dynamic import of only *Schema constants to core', () => { const input = [`const { CallToolResultSchema } = await import('@modelcontextprotocol/sdk/types.js');`, ''].join('\n'); const result = applyTransform(input); - expect(result).toContain(`import('@modelcontextprotocol/sdk-shared')`); + expect(result).toContain(`import('@modelcontextprotocol/core')`); expect(result).not.toContain('@modelcontextprotocol/sdk/types'); }); - it('renames JSONRPCResponseSchema and routes it to sdk-shared in a mock factory', () => { + it('renames JSONRPCResponseSchema and routes it to core in a mock factory', () => { const input = [`vi.mock('@modelcontextprotocol/sdk/types.js', () => ({`, ` JSONRPCResponseSchema: vi.fn()`, `}));`, ''].join( '\n' ); const result = applyTransform(input); - expect(result).toContain(`'@modelcontextprotocol/sdk-shared'`); + expect(result).toContain(`'@modelcontextprotocol/core'`); expect(result).toContain('JSONRPCResultResponseSchema'); expect(result).not.toMatch(/(? ({ content: [{ type: 'text', text: `Hello, ${name}!` }] }) - ); - ``` - - For raw JSON Schema (e.g. TypeBox output), use the new `fromJsonSchema` adapter: - - ```typescript - import { fromJsonSchema, AjvJsonSchemaValidator } from '@modelcontextprotocol/core'; - - server.registerTool( - 'greet', - { - inputSchema: fromJsonSchema({ type: 'object', properties: { name: { type: 'string' } } }, new AjvJsonSchemaValidator()) - }, - handler - ); - ``` - - **Breaking changes:** - - `experimental.tasks.getTaskResult()` no longer accepts a `resultSchema` parameter. Returns `GetTaskPayloadResult` (a loose `Result`); cast to the expected type at the call site. - - Removed unused exports from `@modelcontextprotocol/core`: `SchemaInput`, `schemaToJson`, `parseSchemaAsync`, `getSchemaShape`, `getSchemaDescription`, `isOptionalSchema`, `unwrapOptionalSchema`. Use the new `standardSchemaToJsonSchema` and `validateStandardSchema` instead. - - `completable()` remains Zod-specific (it relies on Zod's `.shape` introspection). - -### Patch Changes - -- [#1735](https://github.com/modelcontextprotocol/typescript-sdk/pull/1735) [`a2e5037`](https://github.com/modelcontextprotocol/typescript-sdk/commit/a2e503733f6f3eea3a79a80bdc1b3cdd743f8bb3) Thanks [@felixweinberger](https://github.com/felixweinberger)! - Abort in-flight request - handlers when the connection closes. Previously, request handlers would continue running after the transport disconnected, wasting resources and preventing proper cleanup. Also fixes `InMemoryTransport.close()` firing `onclose` twice on the initiating side. - -- [#1574](https://github.com/modelcontextprotocol/typescript-sdk/pull/1574) [`379392d`](https://github.com/modelcontextprotocol/typescript-sdk/commit/379392d04460ee2cbeecae374901fae21e525031) Thanks [@olaservo](https://github.com/olaservo)! - Add missing `size` field to - `ResourceSchema` to match the MCP specification - -- [#1363](https://github.com/modelcontextprotocol/typescript-sdk/pull/1363) [`0a75810`](https://github.com/modelcontextprotocol/typescript-sdk/commit/0a75810b26e24bae6b9cfb41e12ac770aeaa1da4) Thanks [@DevJanderson](https://github.com/DevJanderson)! - Fix ReDoS vulnerability in - UriTemplate regex patterns (CVE-2026-0621) - -- [#1761](https://github.com/modelcontextprotocol/typescript-sdk/pull/1761) [`01954e6`](https://github.com/modelcontextprotocol/typescript-sdk/commit/01954e621afe525cc3c1bbe8d781e44734cf81c2) Thanks [@felixweinberger](https://github.com/felixweinberger)! - Convert remaining - capability-assertion throws to `SdkError(SdkErrorCode.CapabilityNotSupported, ...)`. Follow-up to #1454 which missed `Client.assertCapability()`, the task capability helpers in `experimental/tasks/helpers.ts`, and the sampling/elicitation capability checks in - `experimental/tasks/server.ts`. - -- [#1790](https://github.com/modelcontextprotocol/typescript-sdk/pull/1790) [`89fb094`](https://github.com/modelcontextprotocol/typescript-sdk/commit/89fb0947b487b37f9bfcc2a2486dcd33d3922f8e) Thanks [@felixweinberger](https://github.com/felixweinberger)! - Consolidate per-request - cleanup in `_requestWithSchema` into a single `.finally()` block. This fixes an abort signal listener leak (listeners accumulated when a caller reused one `AbortSignal` across requests) and two cases where `_responseHandlers` entries leaked on send-failure paths. - -- [#1486](https://github.com/modelcontextprotocol/typescript-sdk/pull/1486) [`65bbcea`](https://github.com/modelcontextprotocol/typescript-sdk/commit/65bbceab773277f056a9d3e385e7e7d8cef54f9b) Thanks [@localden](https://github.com/localden)! - Fix InMemoryTaskStore to enforce - session isolation. Previously, sessionId was accepted but ignored on all TaskStore methods, allowing any session to enumerate, read, and mutate tasks created by other sessions. The store now persists sessionId at creation time and enforces ownership on all reads and writes. - -- [#1766](https://github.com/modelcontextprotocol/typescript-sdk/pull/1766) [`48aba0d`](https://github.com/modelcontextprotocol/typescript-sdk/commit/48aba0d3c3b2ee04c442934095b663d19e07a3b3) Thanks [@felixweinberger](https://github.com/felixweinberger)! - Add explicit - `| undefined` to optional properties on the `Transport` interface and `TransportSendOptions` (`onclose`, `onerror`, `onmessage`, `sessionId`, `setProtocolVersion`, `setSupportedProtocolVersions`, `onresumptiontoken`). - - This fixes TS2420 errors for consumers using `exactOptionalPropertyTypes: true` without `skipLibCheck`, where the emitted `.d.ts` for implementing classes included `| undefined` but the interface did not. - - Workaround for older SDK versions: enable `skipLibCheck: true` in your tsconfig. - -- [#1419](https://github.com/modelcontextprotocol/typescript-sdk/pull/1419) [`dcf708d`](https://github.com/modelcontextprotocol/typescript-sdk/commit/dcf708d892b7ca5f137c74109d42cdeb05e2ee3a) Thanks [@KKonstantinov](https://github.com/KKonstantinov)! - remove deprecated .tool, - .prompt, .resource method signatures - -- [#1534](https://github.com/modelcontextprotocol/typescript-sdk/pull/1534) [`69a0626`](https://github.com/modelcontextprotocol/typescript-sdk/commit/69a062693f61e024d7a366db0c3e3ba74ff59d8e) Thanks [@josefaidt](https://github.com/josefaidt)! - remove npm references, use pnpm - -- [#1534](https://github.com/modelcontextprotocol/typescript-sdk/pull/1534) [`69a0626`](https://github.com/modelcontextprotocol/typescript-sdk/commit/69a062693f61e024d7a366db0c3e3ba74ff59d8e) Thanks [@josefaidt](https://github.com/josefaidt)! - clean up package manager usage, all - pnpm - -- [#1796](https://github.com/modelcontextprotocol/typescript-sdk/pull/1796) [`d6a02c8`](https://github.com/modelcontextprotocol/typescript-sdk/commit/d6a02c85c0514658c27615398a3003aadce80fb0) Thanks [@felixweinberger](https://github.com/felixweinberger)! - Ensure - `standardSchemaToJsonSchema` emits `type: "object"` at the root, fixing discriminated-union tool/prompt schemas that previously produced `{oneOf: [...]}` without the MCP-required top-level type. Also throws a clear error when given an explicitly non-object schema (e.g. - `z.string()`). Fixes #1643. - -- [#1419](https://github.com/modelcontextprotocol/typescript-sdk/pull/1419) [`dcf708d`](https://github.com/modelcontextprotocol/typescript-sdk/commit/dcf708d892b7ca5f137c74109d42cdeb05e2ee3a) Thanks [@KKonstantinov](https://github.com/KKonstantinov)! - deprecated .tool, .prompt, - .resource method removal - -- [#1762](https://github.com/modelcontextprotocol/typescript-sdk/pull/1762) [`64897f7`](https://github.com/modelcontextprotocol/typescript-sdk/commit/64897f78ce78f736b027dfecd1b4326c8c6678c7) Thanks [@felixweinberger](https://github.com/felixweinberger)! - - `ReadBuffer.readMessage()` now silently skips non-JSON lines instead of throwing `SyntaxError`. This prevents noisy `onerror` callbacks when hot-reload tools (tsx, nodemon) write debug output like "Gracefully restarting..." to stdout. Lines that parse as JSON but fail JSONRPC - schema validation still throw. diff --git a/packages/sdk-shared/README.md b/packages/core/README.md similarity index 85% rename from packages/sdk-shared/README.md rename to packages/core/README.md index 3902f4db19..5da63a8f2a 100644 --- a/packages/sdk-shared/README.md +++ b/packages/core/README.md @@ -1,4 +1,4 @@ -# @modelcontextprotocol/sdk-shared +# @modelcontextprotocol/core Canonical public home for the [Model Context Protocol](https://modelcontextprotocol.io) specification and OAuth/OpenID **Zod schemas**. @@ -8,13 +8,13 @@ raw schemas when you need to validate or parse MCP messages yourself. ## Install ```sh -npm install @modelcontextprotocol/sdk-shared +npm install @modelcontextprotocol/core ``` ## Usage ```ts -import { CallToolResultSchema } from '@modelcontextprotocol/sdk-shared'; +import { CallToolResultSchema } from '@modelcontextprotocol/core'; // Throws on invalid input; returns the typed result on success. const result = CallToolResultSchema.parse(payload); @@ -36,5 +36,5 @@ This package exports **only** Zod schema constants (`*Schema`), in two groups: The corresponding TypeScript types, error classes, enums, and type guards are part of the public API of [`@modelcontextprotocol/server`](https://www.npmjs.com/package/@modelcontextprotocol/server) and [`@modelcontextprotocol/client`](https://www.npmjs.com/package/@modelcontextprotocol/client). -> **Migrating from v1?** In v1 these schemas were imported from `@modelcontextprotocol/sdk/types.js` (spec schemas) and `@modelcontextprotocol/sdk/shared/auth.js` (OAuth/OpenID schemas). Point those `*Schema` imports at `@modelcontextprotocol/sdk-shared` and your existing -> `.parse()` / `.safeParse()` calls keep working unchanged. +> **Migrating from v1?** In v1 these schemas were imported from `@modelcontextprotocol/sdk/types.js` (spec schemas) and `@modelcontextprotocol/sdk/shared/auth.js` (OAuth/OpenID schemas). Point those `*Schema` imports at `@modelcontextprotocol/core` and your existing `.parse()` / +> `.safeParse()` calls keep working unchanged. diff --git a/packages/core/eslint.config.mjs b/packages/core/eslint.config.mjs index 951c9f3a91..dd9e88588a 100644 --- a/packages/core/eslint.config.mjs +++ b/packages/core/eslint.config.mjs @@ -2,4 +2,11 @@ import baseConfig from '@modelcontextprotocol/eslint-config'; -export default baseConfig; +export default [ + ...baseConfig, + { + settings: { + 'import/internal-regex': '^@modelcontextprotocol/core-internal' + } + } +]; diff --git a/packages/core/package.json b/packages/core/package.json index 360f37ea1c..5126810722 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,8 +1,7 @@ { "name": "@modelcontextprotocol/core", - "private": true, - "version": "2.0.0-alpha.1", - "description": "Model Context Protocol implementation for TypeScript - Core package", + "version": "2.0.0-alpha.0", + "description": "Model Context Protocol for TypeScript — public Zod schemas (spec + OAuth/OpenID)", "license": "MIT", "author": "Anthropic, PBC (https://anthropic.com)", "homepage": "https://modelcontextprotocol.io", @@ -18,32 +17,24 @@ "keywords": [ "modelcontextprotocol", "mcp", - "core" + "schemas", + "zod" ], "exports": { ".": { "types": "./dist/index.d.ts", - "import": "./dist/index.mjs" - }, - "./types": { - "types": "./src/exports/types/index.ts", - "import": "./src/exports/types/index.ts" - }, - "./public": { - "types": "./src/exports/public/index.ts", - "import": "./src/exports/public/index.ts" - }, - "./validators/ajv": { - "types": "./src/validators/ajvProvider.ts", - "import": "./src/validators/ajvProvider.ts" - }, - "./validators/cfWorker": { - "types": "./src/validators/cfWorkerProvider.ts", - "import": "./src/validators/cfWorkerProvider.ts" + "import": "./dist/index.js" } }, + "types": "./dist/index.d.ts", + "files": [ + "dist" + ], "scripts": { "typecheck": "tsgo -p tsconfig.json --noEmit", + "build": "tsdown", + "build:watch": "tsdown --watch", + "prepack": "pnpm run build", "lint": "eslint src/ && prettier --ignore-path ../../.prettierignore --check .", "lint:fix": "eslint src/ --fix && prettier --ignore-path ../../.prettierignore --write .", "check": "pnpm run typecheck && pnpm run lint", @@ -51,48 +42,20 @@ "test:watch": "vitest" }, "dependencies": { - "json-schema-typed": "catalog:runtimeShared", - "zod": "catalog:runtimeShared" - }, - "peerDependencies": { - "@cfworker/json-schema": "catalog:runtimeShared", - "ajv": "catalog:runtimeShared", - "ajv-formats": "catalog:runtimeShared", "zod": "catalog:runtimeShared" }, - "peerDependenciesMeta": { - "@cfworker/json-schema": { - "optional": true - }, - "ajv": { - "optional": true - }, - "ajv-formats": { - "optional": true - }, - "zod": { - "optional": false - } - }, "devDependencies": { + "@eslint/js": "catalog:devTools", + "@modelcontextprotocol/core-internal": "workspace:^", + "@modelcontextprotocol/eslint-config": "workspace:^", "@modelcontextprotocol/tsconfig": "workspace:^", "@modelcontextprotocol/vitest-config": "workspace:^", - "@modelcontextprotocol/eslint-config": "workspace:^", - "@cfworker/json-schema": "catalog:runtimeShared", - "ajv": "catalog:runtimeShared", - "ajv-formats": "catalog:runtimeShared", - "@eslint/js": "catalog:devTools", - "@types/content-type": "catalog:devTools", - "@types/cors": "catalog:devTools", - "@types/cross-spawn": "catalog:devTools", - "@types/eventsource": "catalog:devTools", - "@types/express": "catalog:devTools", - "@types/express-serve-static-core": "catalog:devTools", "@typescript/native-preview": "catalog:devTools", "eslint": "catalog:devTools", "eslint-config-prettier": "catalog:devTools", "eslint-plugin-n": "catalog:devTools", "prettier": "catalog:devTools", + "tsdown": "catalog:devTools", "typescript": "catalog:devTools", "typescript-eslint": "catalog:devTools", "vitest": "catalog:devTools" diff --git a/packages/core/src/auth/errors.ts b/packages/core/src/auth/errors.ts deleted file mode 100644 index f2060887f1..0000000000 --- a/packages/core/src/auth/errors.ts +++ /dev/null @@ -1,132 +0,0 @@ -import type { OAuthErrorResponse } from '../shared/auth'; - -/** - * OAuth error codes as defined by {@link https://datatracker.ietf.org/doc/html/rfc6749#section-5.2 | RFC 6749} - * and extensions. - */ -export enum OAuthErrorCode { - /** - * The request is missing a required parameter, includes an invalid parameter value, - * includes a parameter more than once, or is otherwise malformed. - */ - InvalidRequest = 'invalid_request', - - /** - * Client authentication failed (e.g., unknown client, no client authentication included, - * or unsupported authentication method). - */ - InvalidClient = 'invalid_client', - - /** - * The provided authorization grant or refresh token is invalid, expired, revoked, - * does not match the redirection URI used in the authorization request, or was issued to another client. - */ - InvalidGrant = 'invalid_grant', - - /** - * The authenticated client is not authorized to use this authorization grant type. - */ - UnauthorizedClient = 'unauthorized_client', - - /** - * The authorization grant type is not supported by the authorization server. - */ - UnsupportedGrantType = 'unsupported_grant_type', - - /** - * The requested scope is invalid, unknown, malformed, or exceeds the scope granted by the resource owner. - */ - InvalidScope = 'invalid_scope', - - /** - * The resource owner or authorization server denied the request. - */ - AccessDenied = 'access_denied', - - /** - * The authorization server encountered an unexpected condition that prevented it from fulfilling the request. - */ - ServerError = 'server_error', - - /** - * The authorization server is currently unable to handle the request due to temporary overloading or maintenance. - */ - TemporarilyUnavailable = 'temporarily_unavailable', - - /** - * The authorization server does not support obtaining an authorization code using this method. - */ - UnsupportedResponseType = 'unsupported_response_type', - - /** - * The authorization server does not support the requested token type. - */ - UnsupportedTokenType = 'unsupported_token_type', - - /** - * The access token provided is expired, revoked, malformed, or invalid for other reasons. - */ - InvalidToken = 'invalid_token', - - /** - * The HTTP method used is not allowed for this endpoint. (Custom, non-standard error) - */ - MethodNotAllowed = 'method_not_allowed', - - /** - * Rate limit exceeded. (Custom, non-standard error based on RFC 6585) - */ - TooManyRequests = 'too_many_requests', - - /** - * The client metadata is invalid. (Custom error for dynamic client registration - RFC 7591) - */ - InvalidClientMetadata = 'invalid_client_metadata', - - /** - * The request requires higher privileges than provided by the access token. - */ - InsufficientScope = 'insufficient_scope', - - /** - * The requested resource is invalid, missing, unknown, or malformed. (Custom error for resource indicators - RFC 8707) - */ - InvalidTarget = 'invalid_target' -} - -/** - * OAuth error class for all OAuth-related errors. - */ -export class OAuthError extends Error { - constructor( - public readonly code: OAuthErrorCode | string, - message: string, - public readonly errorUri?: string - ) { - super(message); - this.name = 'OAuthError'; - } - - /** - * Converts the error to a standard OAuth error response object. - */ - toResponseObject(): OAuthErrorResponse { - const response: OAuthErrorResponse = { - error: this.code, - error_description: this.message - }; - - if (this.errorUri) { - response.error_uri = this.errorUri; - } - - return response; - } - - /** - * Creates an {@linkcode OAuthError} from an OAuth error response. - */ - static fromResponse(response: OAuthErrorResponse): OAuthError { - return new OAuthError(response.error as OAuthErrorCode, response.error_description ?? response.error, response.error_uri); - } -} diff --git a/packages/core/src/errors/sdkErrors.examples.ts b/packages/core/src/errors/sdkErrors.examples.ts deleted file mode 100644 index c828d443ec..0000000000 --- a/packages/core/src/errors/sdkErrors.examples.ts +++ /dev/null @@ -1,39 +0,0 @@ -/** - * Type-checked examples for `sdkErrors.ts`. - * - * These examples are synced into JSDoc comments via the sync-snippets script. - * Each function's region markers define the code snippet that appears in the docs. - * - * @module - */ - -import { SdkError, SdkErrorCode, SdkHttpError } from './sdkErrors'; - -/** - * Example: Throwing and catching SDK errors. - */ -function SdkError_basicUsage() { - //#region SdkError_basicUsage - try { - // Throwing an SDK error - throw new SdkError(SdkErrorCode.NotConnected, 'Transport is not connected'); - } catch (error) { - // Checking error type by code - if (error instanceof SdkError && error.code === SdkErrorCode.RequestTimeout) { - // Handle timeout - } - } - //#endregion SdkError_basicUsage -} - -/** - * Example: Checking for HTTP transport errors. - */ -function SdkHttpError_basicUsage(error: unknown) { - //#region SdkHttpError_basicUsage - if (error instanceof SdkHttpError) { - console.log(error.status); // number - console.log(error.statusText); // string | undefined - } - //#endregion SdkHttpError_basicUsage -} diff --git a/packages/core/src/errors/sdkErrors.ts b/packages/core/src/errors/sdkErrors.ts deleted file mode 100644 index af432c6389..0000000000 --- a/packages/core/src/errors/sdkErrors.ts +++ /dev/null @@ -1,110 +0,0 @@ -/** - * Error codes for SDK errors (local errors that never cross the wire). - * Unlike {@linkcode ProtocolErrorCode} which uses numeric JSON-RPC codes, `SdkErrorCode` uses - * descriptive string values for better developer experience. - * - * These errors are thrown locally by the SDK and are never serialized as - * JSON-RPC error responses. - */ -export enum SdkErrorCode { - // State errors - /** Transport is not connected */ - NotConnected = 'NOT_CONNECTED', - /** Transport is already connected */ - AlreadyConnected = 'ALREADY_CONNECTED', - /** Protocol is not initialized */ - NotInitialized = 'NOT_INITIALIZED', - - // Capability errors - /** Required capability is not supported by the remote side */ - CapabilityNotSupported = 'CAPABILITY_NOT_SUPPORTED', - - // Transport errors - /** Request timed out waiting for response */ - RequestTimeout = 'REQUEST_TIMEOUT', - /** Connection was closed */ - ConnectionClosed = 'CONNECTION_CLOSED', - /** Failed to send message */ - SendFailed = 'SEND_FAILED', - /** Response result failed local schema validation */ - InvalidResult = 'INVALID_RESULT', - - // Transport errors - ClientHttpNotImplemented = 'CLIENT_HTTP_NOT_IMPLEMENTED', - ClientHttpAuthentication = 'CLIENT_HTTP_AUTHENTICATION', - ClientHttpForbidden = 'CLIENT_HTTP_FORBIDDEN', - ClientHttpUnexpectedContent = 'CLIENT_HTTP_UNEXPECTED_CONTENT', - ClientHttpFailedToOpenStream = 'CLIENT_HTTP_FAILED_TO_OPEN_STREAM', - ClientHttpFailedToTerminateSession = 'CLIENT_HTTP_FAILED_TO_TERMINATE_SESSION' -} - -/** - * SDK errors are local errors that never cross the wire. - * They are distinct from {@linkcode ProtocolError} which represents JSON-RPC protocol errors - * that are serialized and sent as error responses. - * - * @example - * ```ts source="./sdkErrors.examples.ts#SdkError_basicUsage" - * try { - * // Throwing an SDK error - * throw new SdkError(SdkErrorCode.NotConnected, 'Transport is not connected'); - * } catch (error) { - * // Checking error type by code - * if (error instanceof SdkError && error.code === SdkErrorCode.RequestTimeout) { - * // Handle timeout - * } - * } - * ``` - */ -export class SdkError extends Error { - constructor( - public readonly code: SdkErrorCode, - message: string, - public readonly data?: unknown - ) { - super(message); - this.name = 'SdkError'; - } -} - -/** - * Typed shape for HTTP error data carried by {@linkcode SdkHttpError}. - */ -export interface SdkHttpErrorData { - status: number; - statusText?: string; - [key: string]: unknown; -} - -/** - * An {@linkcode SdkError} subclass for HTTP transport failures. - * - * Thrown by the streamable HTTP transport when the server responds with a - * non-OK status code. Narrows {@linkcode SdkError.data | data} to - * {@linkcode SdkHttpErrorData} so consumers can inspect the HTTP status - * without unsafe casting. - * - * @example - * ```ts source="./sdkErrors.examples.ts#SdkHttpError_basicUsage" - * if (error instanceof SdkHttpError) { - * console.log(error.status); // number - * console.log(error.statusText); // string | undefined - * } - * ``` - */ -export class SdkHttpError extends SdkError { - declare readonly data: SdkHttpErrorData; - - constructor(code: SdkErrorCode, message: string, data: SdkHttpErrorData) { - super(code, message, data); - this.name = 'SdkHttpError'; - } - - get status(): number { - return this.data.status; - } - - get statusText(): string | undefined { - return this.data.statusText; - } -} diff --git a/packages/core/src/exports/public/index.ts b/packages/core/src/exports/public/index.ts deleted file mode 100644 index 67f1d9aff3..0000000000 --- a/packages/core/src/exports/public/index.ts +++ /dev/null @@ -1,126 +0,0 @@ -/** - * Curated public API exports for @modelcontextprotocol/core. - * - * This module defines the stable, public-facing API surface. Client and server - * packages re-export from here so that end users only see supported symbols. - * - * Internal utilities (Protocol class, stdio parsing, schema helpers, etc.) - * remain available via the internal barrel (@modelcontextprotocol/core) for - * use by client/server packages. - */ - -// Auth error classes -export { OAuthError, OAuthErrorCode } from '../../auth/errors'; - -// SDK error types (local errors that never cross the wire) -export type { SdkHttpErrorData } from '../../errors/sdkErrors'; -export { SdkError, SdkErrorCode, SdkHttpError } from '../../errors/sdkErrors'; - -// Auth TypeScript types (NOT Zod schemas like OAuthMetadataSchema) -export type { - AuthorizationServerMetadata, - IdJagTokenExchangeResponse, - OAuthClientInformation, - OAuthClientInformationFull, - OAuthClientInformationMixed, - OAuthClientMetadata, - OAuthClientRegistrationError, - OAuthErrorResponse, - OAuthMetadata, - OAuthProtectedResourceMetadata, - OAuthTokenRevocationRequest, - OAuthTokens, - OpenIdProviderDiscoveryMetadata, - OpenIdProviderMetadata -} from '../../shared/auth'; - -// Auth utilities -export { checkResourceAllowed, resourceUrlFromServerUrl } from '../../shared/authUtils'; - -// Metadata utilities -export { getDisplayName } from '../../shared/metadataUtils'; - -// Protocol types (NOT the Protocol class itself or mergeCapabilities) -export type { - BaseContext, - ClientContext, - NotificationOptions, - ProgressCallback, - ProtocolOptions, - RequestHandlerSchemas, - RequestOptions, - ServerContext -} from '../../shared/protocol'; -export { DEFAULT_REQUEST_TIMEOUT_MSEC } from '../../shared/protocol'; - -// stdio message framing utilities (for custom transport authors) -export { deserializeMessage, ReadBuffer, serializeMessage, STDIO_DEFAULT_MAX_BUFFER_SIZE } from '../../shared/stdio'; - -// Transport types (NOT normalizeHeaders) -export type { FetchLike, Transport, TransportSendOptions } from '../../shared/transport'; -export { createFetchWithInit } from '../../shared/transport'; -export { InMemoryTransport } from '../../util/inMemory'; - -// URI Template -export type { Variables } from '../../shared/uriTemplate'; -export { UriTemplate } from '../../shared/uriTemplate'; - -// Types — all TypeScript types (standalone interfaces + schema-derived). -// This is the one intentional `export *`: types.ts contains only spec-derived TS -// types, and every type there should be public. See comment in types.ts. -export * from '../../types/types'; - -// Constants -export { - BAGGAGE_META_KEY, - CLIENT_CAPABILITIES_META_KEY, - CLIENT_INFO_META_KEY, - DEFAULT_NEGOTIATED_PROTOCOL_VERSION, - INTERNAL_ERROR, - INVALID_PARAMS, - INVALID_REQUEST, - JSONRPC_VERSION, - LATEST_PROTOCOL_VERSION, - LOG_LEVEL_META_KEY, - METHOD_NOT_FOUND, - PARSE_ERROR, - PROTOCOL_VERSION_META_KEY, - RELATED_TASK_META_KEY, - SUPPORTED_PROTOCOL_VERSIONS, - TRACEPARENT_META_KEY, - TRACESTATE_META_KEY -} from '../../types/constants'; - -// Enums -export { ProtocolErrorCode } from '../../types/enums'; - -// Error classes -export { ProtocolError, UnsupportedProtocolVersionError, UrlElicitationRequiredError } from '../../types/errors'; - -// Type guards and message parsing -export { - assertCompleteRequestPrompt, - assertCompleteRequestResourceTemplate, - isCallToolResult, - isInitializedNotification, - isInitializeRequest, - isJSONRPCErrorResponse, - isJSONRPCNotification, - isJSONRPCRequest, - isJSONRPCResponse, - isJSONRPCResultResponse, - isTaskAugmentedRequestParams, - parseJSONRPCMessage -} from '../../types/guards'; - -// Validator types and classes -export type { SpecTypeName, SpecTypes } from '../../types/specTypeSchema'; -export { isSpecType, specTypeSchemas } from '../../types/specTypeSchema'; -export type { StandardSchemaV1, StandardSchemaV1Sync, StandardSchemaWithJSON } from '../../util/standardSchema'; -// Validator providers are type-only here — import the runtime classes from the explicit -// `@modelcontextprotocol/{client,server}/validators/{ajv,cf-worker}` subpaths to customise. -export type { AjvJsonSchemaValidator } from '../../validators/ajvProvider'; -export type { CfWorkerJsonSchemaValidator, CfWorkerSchemaDraft } from '../../validators/cfWorkerProvider'; -// fromJsonSchema is intentionally NOT exported here — the server and client packages -// provide runtime-aware wrappers that default to the appropriate validator via _shims. -export type { JsonSchemaType, JsonSchemaValidator, jsonSchemaValidator, JsonSchemaValidatorResult } from '../../validators/types'; diff --git a/packages/core/src/exports/types/index.ts b/packages/core/src/exports/types/index.ts deleted file mode 100644 index cfd18a47ce..0000000000 --- a/packages/core/src/exports/types/index.ts +++ /dev/null @@ -1 +0,0 @@ -export type * from '../../types/index'; diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 940ab08187..2bfd2be28a 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -1,22 +1,198 @@ -export * from './auth/errors'; -export * from './errors/sdkErrors'; -export * from './shared/auth'; -export * from './shared/authUtils'; -export * from './shared/metadataUtils'; -export * from './shared/protocol'; -export * from './shared/stdio'; -export * from './shared/toolNameValidation'; -export * from './shared/transport'; -export * from './shared/uriTemplate'; -export * from './types/index'; -export * from './util/inMemory'; -export * from './util/schema'; -export * from './util/standardSchema'; -export * from './util/zodCompat'; +// @modelcontextprotocol/core +// +// Canonical public home for the Model Context Protocol specification + OAuth/OpenID Zod schemas. +// +// These are the exact schema constants the SDK validates against internally (defined in the +// private @modelcontextprotocol/core-internal package). This package bundles core and re-exports ONLY the +// `*Schema` Zod values, so consumers can validate protocol/OAuth payloads directly — e.g. +// `CallToolResultSchema.parse(value)` / `.safeParse(value)` — without depending on core's +// internal barrel. +// +// Scope: Zod schemas ONLY. The corresponding spec TypeScript types, error classes, enums, and +// type guards are part of the public API of @modelcontextprotocol/server and /client. +// +// Two groups, kept separate to mirror core's own spec-vs-auth split, each bundled from a build-only +// subpath alias of core (tsconfig.json + tsdown.config.ts): +// - SPEC schemas, from @modelcontextprotocol/core-internal/schemas (core/src/types/schemas.ts): every +// `export const *Schema` EXCEPT internal helpers with no public spec type (e.g. +// BaseRequestParamsSchema). Mirrors core's SPEC_SCHEMA_KEYS allowlist. +// - OAUTH/OPENID schemas, from @modelcontextprotocol/core-internal/auth (core/src/shared/auth.ts). +// The sdkSharedSchemas test asserts both groups stay in sync with their core source modules. +export { + AnnotationsSchema, + AudioContentSchema, + BaseMetadataSchema, + BlobResourceContentsSchema, + BooleanSchemaSchema, + CallToolRequestParamsSchema, + CallToolRequestSchema, + CallToolResultSchema, + CancelledNotificationParamsSchema, + CancelledNotificationSchema, + CancelTaskRequestSchema, + CancelTaskResultSchema, + ClientCapabilitiesSchema, + ClientNotificationSchema, + ClientRequestSchema, + ClientResultSchema, + CompatibilityCallToolResultSchema, + CompleteRequestParamsSchema, + CompleteRequestSchema, + CompleteResultSchema, + ContentBlockSchema, + CreateMessageRequestParamsSchema, + CreateMessageRequestSchema, + CreateMessageResultSchema, + CreateMessageResultWithToolsSchema, + CreateTaskResultSchema, + CursorSchema, + DiscoverRequestSchema, + DiscoverResultSchema, + ElicitationCompleteNotificationParamsSchema, + ElicitationCompleteNotificationSchema, + ElicitRequestFormParamsSchema, + ElicitRequestParamsSchema, + ElicitRequestSchema, + ElicitRequestURLParamsSchema, + ElicitResultSchema, + EmbeddedResourceSchema, + EmptyResultSchema, + EnumSchemaSchema, + GetPromptRequestParamsSchema, + GetPromptRequestSchema, + GetPromptResultSchema, + GetTaskPayloadRequestSchema, + GetTaskPayloadResultSchema, + GetTaskRequestSchema, + GetTaskResultSchema, + IconSchema, + IconsSchema, + ImageContentSchema, + ImplementationSchema, + InitializedNotificationSchema, + InitializeRequestParamsSchema, + InitializeRequestSchema, + InitializeResultSchema, + JSONArraySchema, + JSONObjectSchema, + JSONRPCErrorResponseSchema, + JSONRPCMessageSchema, + JSONRPCNotificationSchema, + JSONRPCRequestSchema, + JSONRPCResponseSchema, + JSONRPCResultResponseSchema, + JSONValueSchema, + LegacyTitledEnumSchemaSchema, + ListPromptsRequestSchema, + ListPromptsResultSchema, + ListResourcesRequestSchema, + ListResourcesResultSchema, + ListResourceTemplatesRequestSchema, + ListResourceTemplatesResultSchema, + ListRootsRequestSchema, + ListRootsResultSchema, + ListTasksRequestSchema, + ListTasksResultSchema, + ListToolsRequestSchema, + ListToolsResultSchema, + LoggingLevelSchema, + LoggingMessageNotificationParamsSchema, + LoggingMessageNotificationSchema, + ModelHintSchema, + ModelPreferencesSchema, + MultiSelectEnumSchemaSchema, + NotificationSchema, + NumberSchemaSchema, + PaginatedRequestParamsSchema, + PaginatedRequestSchema, + PaginatedResultSchema, + PingRequestSchema, + PrimitiveSchemaDefinitionSchema, + ProgressNotificationParamsSchema, + ProgressNotificationSchema, + ProgressSchema, + ProgressTokenSchema, + PromptArgumentSchema, + PromptListChangedNotificationSchema, + PromptMessageSchema, + PromptReferenceSchema, + PromptSchema, + ReadResourceRequestParamsSchema, + ReadResourceRequestSchema, + ReadResourceResultSchema, + RelatedTaskMetadataSchema, + RequestIdSchema, + RequestMetaEnvelopeSchema, + RequestMetaSchema, + RequestSchema, + ResourceContentsSchema, + ResourceLinkSchema, + ResourceListChangedNotificationSchema, + ResourceRequestParamsSchema, + ResourceSchema, + ResourceTemplateReferenceSchema, + ResourceTemplateSchema, + ResourceUpdatedNotificationParamsSchema, + ResourceUpdatedNotificationSchema, + ResultSchema, + RoleSchema, + RootSchema, + RootsListChangedNotificationSchema, + SamplingContentSchema, + SamplingMessageContentBlockSchema, + SamplingMessageSchema, + ServerCapabilitiesSchema, + ServerNotificationSchema, + ServerRequestSchema, + ServerResultSchema, + SetLevelRequestParamsSchema, + SetLevelRequestSchema, + SingleSelectEnumSchemaSchema, + StringSchemaSchema, + SubscribeRequestParamsSchema, + SubscribeRequestSchema, + TaskAugmentedRequestParamsSchema, + TaskCreationParamsSchema, + TaskMetadataSchema, + TaskSchema, + TaskStatusNotificationParamsSchema, + TaskStatusNotificationSchema, + TaskStatusSchema, + TextContentSchema, + TextResourceContentsSchema, + TitledMultiSelectEnumSchemaSchema, + TitledSingleSelectEnumSchemaSchema, + ToolAnnotationsSchema, + ToolChoiceSchema, + ToolExecutionSchema, + ToolListChangedNotificationSchema, + ToolResultContentSchema, + ToolSchema, + ToolUseContentSchema, + UnsubscribeRequestParamsSchema, + UnsubscribeRequestSchema, + UntitledMultiSelectEnumSchemaSchema, + UntitledSingleSelectEnumSchemaSchema +} from '@modelcontextprotocol/core-internal/schemas'; -// Validator providers are type-only here — import the runtime classes from the explicit -// `@modelcontextprotocol/{core,client,server}/validators/{ajv,cf-worker}` subpaths to customise. -export type { AjvJsonSchemaValidator } from './validators/ajvProvider'; -export type { CfWorkerJsonSchemaValidator, CfWorkerSchemaDraft } from './validators/cfWorkerProvider'; -export * from './validators/fromJsonSchema'; -export type { JsonSchemaType, JsonSchemaValidator, jsonSchemaValidator, JsonSchemaValidatorResult } from './validators/types'; +// Auth schemas (OAuth / OpenID / IdJag) — kept as a SEPARATE group from the MCP spec schemas above, +// mirroring core's own spec-vs-auth split (these live in core/src/shared/auth.ts, not types/schemas.ts, +// and are registered as `authSchemas` in core's specTypeSchema.ts). This group is EXACTLY core's +// `authSchemas` set — every auth schema that has a public spec type (so `isSpecType.OAuthTokens`, +// `isSpecType.IdJagTokenExchangeResponse`, etc. exist). The typeless internal URL field-validators +// (SafeUrlSchema, OptionalSafeUrlSchema) are not auth schemas and stay out. The sdkSharedSchemas test +// asserts this group stays in sync with core's `authSchemas`. +export { + IdJagTokenExchangeResponseSchema, + OAuthClientInformationFullSchema, + OAuthClientInformationSchema, + OAuthClientMetadataSchema, + OAuthClientRegistrationErrorSchema, + OAuthErrorResponseSchema, + OAuthMetadataSchema, + OAuthProtectedResourceMetadataSchema, + OAuthTokenRevocationRequestSchema, + OAuthTokensSchema, + OpenIdProviderDiscoveryMetadataSchema, + OpenIdProviderMetadataSchema +} from '@modelcontextprotocol/core-internal/auth'; diff --git a/packages/core/src/shared/auth.ts b/packages/core/src/shared/auth.ts deleted file mode 100644 index deee583aa1..0000000000 --- a/packages/core/src/shared/auth.ts +++ /dev/null @@ -1,252 +0,0 @@ -import * as z from 'zod/v4'; - -/** - * Reusable URL validation that disallows `javascript:` scheme - */ -export const SafeUrlSchema = z - .url() - .superRefine((val, ctx) => { - if (!URL.canParse(val)) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: 'URL must be parseable', - fatal: true - }); - - return z.NEVER; - } - }) - .refine( - url => { - const u = new URL(url); - return u.protocol !== 'javascript:' && u.protocol !== 'data:' && u.protocol !== 'vbscript:'; - }, - { message: 'URL cannot use javascript:, data:, or vbscript: scheme' } - ); - -/** - * RFC 9728 OAuth Protected Resource Metadata - */ -export const OAuthProtectedResourceMetadataSchema = z.looseObject({ - resource: z.string().url(), - authorization_servers: z.array(SafeUrlSchema).optional(), - jwks_uri: z.string().url().optional(), - scopes_supported: z.array(z.string()).optional(), - bearer_methods_supported: z.array(z.string()).optional(), - resource_signing_alg_values_supported: z.array(z.string()).optional(), - resource_name: z.string().optional(), - resource_documentation: z.string().optional(), - resource_policy_uri: z.string().url().optional(), - resource_tos_uri: z.string().url().optional(), - tls_client_certificate_bound_access_tokens: z.boolean().optional(), - authorization_details_types_supported: z.array(z.string()).optional(), - dpop_signing_alg_values_supported: z.array(z.string()).optional(), - dpop_bound_access_tokens_required: z.boolean().optional() -}); - -/** - * RFC 8414 OAuth 2.0 Authorization Server Metadata - */ -export const OAuthMetadataSchema = z.looseObject({ - issuer: z.string(), - authorization_endpoint: SafeUrlSchema, - token_endpoint: SafeUrlSchema, - registration_endpoint: SafeUrlSchema.optional(), - scopes_supported: z.array(z.string()).optional(), - response_types_supported: z.array(z.string()), - response_modes_supported: z.array(z.string()).optional(), - grant_types_supported: z.array(z.string()).optional(), - token_endpoint_auth_methods_supported: z.array(z.string()).optional(), - token_endpoint_auth_signing_alg_values_supported: z.array(z.string()).optional(), - service_documentation: SafeUrlSchema.optional(), - revocation_endpoint: SafeUrlSchema.optional(), - revocation_endpoint_auth_methods_supported: z.array(z.string()).optional(), - revocation_endpoint_auth_signing_alg_values_supported: z.array(z.string()).optional(), - introspection_endpoint: z.string().optional(), - introspection_endpoint_auth_methods_supported: z.array(z.string()).optional(), - introspection_endpoint_auth_signing_alg_values_supported: z.array(z.string()).optional(), - code_challenge_methods_supported: z.array(z.string()).optional(), - client_id_metadata_document_supported: z.boolean().optional() -}); - -/** - * OpenID Connect Discovery 1.0 Provider Metadata - * - * @see https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata - */ -export const OpenIdProviderMetadataSchema = z.looseObject({ - issuer: z.string(), - authorization_endpoint: SafeUrlSchema, - token_endpoint: SafeUrlSchema, - userinfo_endpoint: SafeUrlSchema.optional(), - jwks_uri: SafeUrlSchema, - registration_endpoint: SafeUrlSchema.optional(), - scopes_supported: z.array(z.string()).optional(), - response_types_supported: z.array(z.string()), - response_modes_supported: z.array(z.string()).optional(), - grant_types_supported: z.array(z.string()).optional(), - acr_values_supported: z.array(z.string()).optional(), - subject_types_supported: z.array(z.string()), - id_token_signing_alg_values_supported: z.array(z.string()), - id_token_encryption_alg_values_supported: z.array(z.string()).optional(), - id_token_encryption_enc_values_supported: z.array(z.string()).optional(), - userinfo_signing_alg_values_supported: z.array(z.string()).optional(), - userinfo_encryption_alg_values_supported: z.array(z.string()).optional(), - userinfo_encryption_enc_values_supported: z.array(z.string()).optional(), - request_object_signing_alg_values_supported: z.array(z.string()).optional(), - request_object_encryption_alg_values_supported: z.array(z.string()).optional(), - request_object_encryption_enc_values_supported: z.array(z.string()).optional(), - token_endpoint_auth_methods_supported: z.array(z.string()).optional(), - token_endpoint_auth_signing_alg_values_supported: z.array(z.string()).optional(), - display_values_supported: z.array(z.string()).optional(), - claim_types_supported: z.array(z.string()).optional(), - claims_supported: z.array(z.string()).optional(), - service_documentation: z.string().optional(), - claims_locales_supported: z.array(z.string()).optional(), - ui_locales_supported: z.array(z.string()).optional(), - claims_parameter_supported: z.boolean().optional(), - request_parameter_supported: z.boolean().optional(), - request_uri_parameter_supported: z.boolean().optional(), - require_request_uri_registration: z.boolean().optional(), - op_policy_uri: SafeUrlSchema.optional(), - op_tos_uri: SafeUrlSchema.optional(), - client_id_metadata_document_supported: z.boolean().optional() -}); - -/** - * OpenID Connect Discovery metadata that may include OAuth 2.0 fields - * This schema represents the real-world scenario where OIDC providers - * return a mix of OpenID Connect and OAuth 2.0 metadata fields - */ -export const OpenIdProviderDiscoveryMetadataSchema = z.object({ - ...OpenIdProviderMetadataSchema.shape, - ...OAuthMetadataSchema.pick({ - code_challenge_methods_supported: true - }).shape -}); - -/** - * OAuth 2.1 token response - */ -export const OAuthTokensSchema = z - .object({ - access_token: z.string(), - id_token: z.string().optional(), // Optional for OAuth 2.1, but necessary in OpenID Connect - token_type: z.string(), - expires_in: z.coerce.number().optional(), - scope: z.string().optional(), - refresh_token: z.string().optional() - }) - .strip(); - -/** - * RFC 8693 §2.2.1 Token Exchange response for ID-JAG tokens. - * - * `token_type` is intentionally optional: per RFC 8693 §2.2.1 it is informational when - * the issued token is not an access token, and per RFC 6749 §5.1 it is case-insensitive, - * so strict checking rejects conformant IdPs. - */ -export const IdJagTokenExchangeResponseSchema = z - .object({ - issued_token_type: z.literal('urn:ietf:params:oauth:token-type:id-jag'), - access_token: z.string(), - token_type: z.string().optional(), - expires_in: z.number().optional(), - scope: z.string().optional() - }) - .strip(); - -export type IdJagTokenExchangeResponse = z.infer; - -/** - * OAuth 2.1 error response - */ -export const OAuthErrorResponseSchema = z.object({ - error: z.string(), - error_description: z.string().optional(), - error_uri: z.string().optional() -}); - -/** - * Optional version of {@linkcode SafeUrlSchema} that allows empty string for backward compatibility on `tos_uri` and `logo_uri` - */ -// eslint-disable-next-line unicorn/no-useless-undefined -export const OptionalSafeUrlSchema = SafeUrlSchema.optional().or(z.literal('').transform(() => undefined)); - -/** - * RFC 7591 OAuth 2.0 Dynamic Client Registration metadata - */ -export const OAuthClientMetadataSchema = z - .object({ - redirect_uris: z.array(SafeUrlSchema), - token_endpoint_auth_method: z.string().optional(), - grant_types: z.array(z.string()).optional(), - response_types: z.array(z.string()).optional(), - client_name: z.string().optional(), - client_uri: SafeUrlSchema.optional(), - logo_uri: OptionalSafeUrlSchema, - scope: z.string().optional(), - contacts: z.array(z.string()).optional(), - tos_uri: OptionalSafeUrlSchema, - policy_uri: z.string().optional(), - jwks_uri: SafeUrlSchema.optional(), - jwks: z.any().optional(), - software_id: z.string().optional(), - software_version: z.string().optional(), - software_statement: z.string().optional() - }) - .strip(); - -/** - * RFC 7591 OAuth 2.0 Dynamic Client Registration client information - */ -export const OAuthClientInformationSchema = z - .object({ - client_id: z.string(), - client_secret: z.string().optional(), - client_id_issued_at: z.number().optional(), - client_secret_expires_at: z.number().optional() - }) - .strip(); - -/** - * RFC 7591 OAuth 2.0 Dynamic Client Registration full response (client information plus metadata) - */ -export const OAuthClientInformationFullSchema = OAuthClientMetadataSchema.merge(OAuthClientInformationSchema); - -/** - * RFC 7591 OAuth 2.0 Dynamic Client Registration error response - */ -export const OAuthClientRegistrationErrorSchema = z - .object({ - error: z.string(), - error_description: z.string().optional() - }) - .strip(); - -/** - * RFC 7009 OAuth 2.0 Token Revocation request - */ -export const OAuthTokenRevocationRequestSchema = z - .object({ - token: z.string(), - token_type_hint: z.string().optional() - }) - .strip(); - -export type OAuthMetadata = z.infer; -export type OpenIdProviderMetadata = z.infer; -export type OpenIdProviderDiscoveryMetadata = z.infer; - -export type OAuthTokens = z.infer; -export type OAuthErrorResponse = z.infer; -export type OAuthClientMetadata = z.infer; -export type OAuthClientInformation = z.infer; -export type OAuthClientInformationFull = z.infer; -export type OAuthClientInformationMixed = OAuthClientInformation | OAuthClientInformationFull; -export type OAuthClientRegistrationError = z.infer; -export type OAuthTokenRevocationRequest = z.infer; -export type OAuthProtectedResourceMetadata = z.infer; - -// Unified type for authorization server metadata -export type AuthorizationServerMetadata = OAuthMetadata | OpenIdProviderDiscoveryMetadata; diff --git a/packages/core/src/shared/authUtils.ts b/packages/core/src/shared/authUtils.ts deleted file mode 100644 index 3083e425b9..0000000000 --- a/packages/core/src/shared/authUtils.ts +++ /dev/null @@ -1,57 +0,0 @@ -/** - * Utilities for handling OAuth resource URIs. - */ - -/** - * Converts a server URL to a resource URL by removing the fragment. - * {@link https://datatracker.ietf.org/doc/html/rfc8707#section-2 | RFC 8707 section 2} - * states that resource URIs "MUST NOT include a fragment component". - * Keeps everything else unchanged (scheme, domain, port, path, query). - */ -export function resourceUrlFromServerUrl(url: URL | string): URL { - const resourceURL = typeof url === 'string' ? new URL(url) : new URL(url.href); - resourceURL.hash = ''; // Remove fragment - return resourceURL; -} - -/** - * Checks if a requested resource URL matches a configured resource URL. - * A requested resource matches if it has the same scheme, domain, port, - * and its path starts with the configured resource's path. - * - * @param options - The options object - * @param options.requestedResource - The resource URL being requested - * @param options.configuredResource - The resource URL that has been configured - * @returns true if the requested resource matches the configured resource, false otherwise - */ -export function checkResourceAllowed({ - requestedResource, - configuredResource -}: { - requestedResource: URL | string; - configuredResource: URL | string; -}): boolean { - const requested = typeof requestedResource === 'string' ? new URL(requestedResource) : new URL(requestedResource.href); - const configured = typeof configuredResource === 'string' ? new URL(configuredResource) : new URL(configuredResource.href); - - // Compare the origin (scheme, domain, and port) - if (requested.origin !== configured.origin) { - return false; - } - - // Handle cases like requested=/foo and configured=/foo/ - if (requested.pathname.length < configured.pathname.length) { - return false; - } - - // Check if the requested path starts with the configured path - // Ensure both paths end with / for proper comparison - // This ensures that if we have paths like "/api" and "/api/users", - // we properly detect that "/api/users" is a subpath of "/api" - // By adding a trailing slash if missing, we avoid false positives - // where paths like "/api123" would incorrectly match "/api" - const requestedPath = requested.pathname.endsWith('/') ? requested.pathname : requested.pathname + '/'; - const configuredPath = configured.pathname.endsWith('/') ? configured.pathname : configured.pathname + '/'; - - return requestedPath.startsWith(configuredPath); -} diff --git a/packages/core/src/shared/metadataUtils.ts b/packages/core/src/shared/metadataUtils.ts deleted file mode 100644 index 0836b4394a..0000000000 --- a/packages/core/src/shared/metadataUtils.ts +++ /dev/null @@ -1,26 +0,0 @@ -import type { BaseMetadata } from '../types/index'; - -/** - * Utilities for working with {@linkcode BaseMetadata} objects. - */ - -/** - * Gets the display name for an object with {@linkcode BaseMetadata}. - * For tools, the precedence is: `title` → {@linkcode index.ToolAnnotations | annotations}.`title` → `name` - * For other objects: `title` → `name` - * This implements the spec requirement: "if no title is provided, name should be used for display purposes" - */ -export function getDisplayName(metadata: BaseMetadata | (BaseMetadata & { annotations?: { title?: string } })): string { - // First check for title (not undefined and not empty string) - if (metadata.title !== undefined && metadata.title !== '') { - return metadata.title; - } - - // Then check for annotations.title (only present in Tool objects) - if ('annotations' in metadata && metadata.annotations?.title) { - return metadata.annotations.title; - } - - // Finally fall back to name - return metadata.name; -} diff --git a/packages/core/src/shared/protocol.examples.ts b/packages/core/src/shared/protocol.examples.ts deleted file mode 100644 index 0ae10e6d08..0000000000 --- a/packages/core/src/shared/protocol.examples.ts +++ /dev/null @@ -1,29 +0,0 @@ -/** - * Type-checked examples for `protocol.ts`. - * - * These examples are synced into JSDoc comments via the sync-snippets script. - * Each function's region markers define the code snippet that appears in the docs. - * - * @module - */ - -import * as z from 'zod/v4'; - -import type { BaseContext, Protocol } from './protocol'; - -/** - * Example: registering a handler for a custom (non-spec) request method. - */ -function Protocol_setRequestHandler_customMethod(protocol: Protocol) { - //#region Protocol_setRequestHandler_customMethod - const SearchParams = z.object({ query: z.string(), limit: z.number().optional() }); - const SearchResult = z.object({ hits: z.array(z.string()) }); - - protocol.setRequestHandler('acme/search', { params: SearchParams, result: SearchResult }, async (params, _ctx) => { - return { hits: [`result for ${params.query}`] }; - }); - //#endregion Protocol_setRequestHandler_customMethod - void protocol; -} - -void Protocol_setRequestHandler_customMethod; diff --git a/packages/core/src/shared/protocol.ts b/packages/core/src/shared/protocol.ts deleted file mode 100644 index 3b7efec2a4..0000000000 --- a/packages/core/src/shared/protocol.ts +++ /dev/null @@ -1,1066 +0,0 @@ -import { SdkError, SdkErrorCode } from '../errors/sdkErrors'; -import type { - AuthInfo, - CancelledNotification, - ClientCapabilities, - CreateMessageRequest, - CreateMessageResult, - CreateMessageResultWithTools, - ElicitRequestFormParams, - ElicitRequestURLParams, - ElicitResult, - JSONRPCErrorResponse, - JSONRPCNotification, - JSONRPCRequest, - JSONRPCResponse, - JSONRPCResultResponse, - LoggingLevel, - MessageExtraInfo, - Notification, - NotificationMethod, - NotificationTypeMap, - Progress, - ProgressNotification, - Request, - RequestId, - RequestMeta, - RequestMethod, - RequestTypeMap, - Result, - ResultTypeMap, - ServerCapabilities -} from '../types/index'; -import { - getNotificationSchema, - getRequestSchema, - getResultSchema, - isJSONRPCErrorResponse, - isJSONRPCNotification, - isJSONRPCRequest, - isJSONRPCResultResponse, - ProtocolError, - ProtocolErrorCode, - SUPPORTED_PROTOCOL_VERSIONS -} from '../types/index'; -import type { StandardSchemaV1 } from '../util/standardSchema'; -import { isStandardSchema, validateStandardSchema } from '../util/standardSchema'; -import type { Transport, TransportSendOptions } from './transport'; - -/** - * Callback for progress notifications. - */ -export type ProgressCallback = (progress: Progress) => void; - -/** - * Additional initialization options. - */ -export type ProtocolOptions = { - /** - * Protocol versions supported. First version is preferred (sent by client, - * used as fallback by server). Passed to transport during {@linkcode Protocol.connect | connect()}. - * - * @default {@linkcode SUPPORTED_PROTOCOL_VERSIONS} - */ - supportedProtocolVersions?: string[]; - - /** - * Whether to restrict emitted requests to only those that the remote side has indicated that they can handle, through their advertised capabilities. - * - * Note that this DOES NOT affect checking of _local_ side capabilities, as it is considered a logic error to mis-specify those. - * - * Currently this defaults to `false`, for backwards compatibility with SDK versions that did not advertise capabilities correctly. In future, this will default to `true`. - */ - enforceStrictCapabilities?: boolean; - /** - * An array of notification method names that should be automatically debounced. - * Any notifications with a method in this list will be coalesced if they - * occur in the same tick of the event loop. - * e.g., `['notifications/tools/list_changed']` - */ - debouncedNotificationMethods?: string[]; -}; - -/** - * The default request timeout, in milliseconds. - */ -export const DEFAULT_REQUEST_TIMEOUT_MSEC = 60_000; - -/** - * Options that can be given per request. - */ -export type RequestOptions = { - /** - * If set, requests progress notifications from the remote end (if supported). When progress notifications are received, this callback will be invoked. - */ - onprogress?: ProgressCallback; - - /** - * Can be used to cancel an in-flight request. This will cause an `AbortError` to be raised from {@linkcode Protocol.request | request()}. - */ - signal?: AbortSignal; - - /** - * A timeout (in milliseconds) for this request. If exceeded, an {@linkcode SdkError} with code {@linkcode SdkErrorCode.RequestTimeout} will be raised from {@linkcode Protocol.request | request()}. - * - * If not specified, {@linkcode DEFAULT_REQUEST_TIMEOUT_MSEC} will be used as the timeout. - */ - timeout?: number; - - /** - * If `true`, receiving a progress notification will reset the request timeout. - * This is useful for long-running operations that send periodic progress updates. - * Default: `false` - */ - resetTimeoutOnProgress?: boolean; - - /** - * Maximum total time (in milliseconds) to wait for a response. - * If exceeded, an {@linkcode SdkError} with code {@linkcode SdkErrorCode.RequestTimeout} will be raised, regardless of progress notifications. - * If not specified, there is no maximum total timeout. - */ - maxTotalTimeout?: number; -} & TransportSendOptions; - -/** - * Options that can be given per notification. - */ -export type NotificationOptions = { - /** - * May be used to indicate to the transport which incoming request to associate this outgoing notification with. - */ - relatedRequestId?: RequestId; -}; - -/** - * Base context provided to all request handlers. - */ -export type BaseContext = { - /** - * The session ID from the transport, if available. - */ - sessionId?: string; - - /** - * Information about the MCP request being handled. - */ - mcpReq: { - /** - * The JSON-RPC ID of the request being handled. - */ - id: RequestId; - - /** - * The method name of the request (e.g., 'tools/call', 'ping'). - */ - method: string; - - /** - * Metadata from the original request. - */ - _meta?: RequestMeta; - - /** - * An abort signal used to communicate if the request was cancelled from the sender's side. - */ - signal: AbortSignal; - - /** - * Sends a request that relates to the current request being handled. - * - * This is used by certain transports to correctly associate related messages. - * - * For spec methods the result type is inferred from the method name. - * For custom (non-spec) methods, pass a result schema as the second argument. - */ - send: { - ( - request: { method: M; params?: Record }, - options?: RequestOptions - ): Promise; - ( - request: Request, - resultSchema: T, - options?: RequestOptions - ): Promise>; - }; - - /** - * Sends a notification that relates to the current request being handled. - * - * This is used by certain transports to correctly associate related messages. - */ - notify: (notification: Notification) => Promise; - }; - - /** - * HTTP transport information, only available when using an HTTP-based transport. - */ - http?: { - /** - * Information about a validated access token, provided to request handlers. - */ - authInfo?: AuthInfo; - }; -}; - -/** - * Context provided to server-side request handlers, extending {@linkcode BaseContext} with server-specific fields. - */ -export type ServerContext = BaseContext & { - mcpReq: { - /** - * Send a log message notification to the client. - * Respects the client's log level filter set via logging/setLevel. - * - * @deprecated Deprecated as of protocol version 2026-07-28 (SEP-2577). - * Remains functional during the deprecation window (at least twelve months). - * Migrate to stderr logging (STDIO servers) or OpenTelemetry. - */ - log: (level: LoggingLevel, data: unknown, logger?: string) => Promise; - - /** - * Send an elicitation request to the client, requesting user input. - */ - elicitInput: (params: ElicitRequestFormParams | ElicitRequestURLParams, options?: RequestOptions) => Promise; - - /** - * Request LLM sampling from the client. - * - * @deprecated Deprecated as of protocol version 2026-07-28 (SEP-2577). - * Remains functional during the deprecation window (at least twelve months). - * Migrate to calling LLM provider APIs directly. - */ - requestSampling: ( - params: CreateMessageRequest['params'], - options?: RequestOptions - ) => Promise; - }; - - http?: { - /** - * The original HTTP request. - */ - req?: globalThis.Request; - - /** - * Closes the SSE stream for this request, triggering client reconnection. - * Only available when using a StreamableHTTPServerTransport with eventStore configured. - */ - closeSSE?: () => void; - - /** - * Closes the standalone GET SSE stream, triggering client reconnection. - * Only available when using a StreamableHTTPServerTransport with eventStore configured. - */ - closeStandaloneSSE?: () => void; - }; -}; - -/** - * Context provided to client-side request handlers. - */ -export type ClientContext = BaseContext; - -/** - * Information about a request's timeout state - */ -type TimeoutInfo = { - timeoutId: ReturnType; - startTime: number; - timeout: number; - maxTotalTimeout?: number; - resetTimeoutOnProgress: boolean; - onTimeout: () => void; -}; - -/** - * Implements MCP protocol framing on top of a pluggable transport, including - * features like request/response linking, notifications, and progress. - * - * `Protocol` is abstract; `Client` and `Server` are the concrete role-specific - * implementations most code should use. - */ -export abstract class Protocol { - private _transport?: Transport; - private _requestMessageId = 0; - private _requestHandlers: Map Promise> = new Map(); - private _requestHandlerAbortControllers: Map = new Map(); - private _notificationHandlers: Map Promise> = new Map(); - private _responseHandlers: Map void> = new Map(); - private _progressHandlers: Map = new Map(); - private _timeoutInfo: Map = new Map(); - private _pendingDebouncedNotifications = new Set(); - - protected _supportedProtocolVersions: string[]; - - /** - * Callback for when the connection is closed for any reason. - * - * This is invoked when {@linkcode Protocol.close | close()} is called as well. - */ - onclose?: () => void; - - /** - * Callback for when an error occurs. - * - * Note that errors are not necessarily fatal; they are used for reporting any kind of exceptional condition out of band. - */ - onerror?: (error: Error) => void; - - /** - * A handler to invoke for any request types that do not have their own handler installed. - */ - fallbackRequestHandler?: (request: JSONRPCRequest, ctx: ContextT) => Promise; - - /** - * A handler to invoke for any notification types that do not have their own handler installed. - */ - fallbackNotificationHandler?: (notification: Notification) => Promise; - - constructor(private _options?: ProtocolOptions) { - this._supportedProtocolVersions = _options?.supportedProtocolVersions ?? SUPPORTED_PROTOCOL_VERSIONS; - - this.setNotificationHandler('notifications/cancelled', notification => { - this._oncancel(notification); - }); - - this.setNotificationHandler('notifications/progress', notification => { - this._onprogress(notification); - }); - - this.setRequestHandler( - 'ping', - // Automatic pong by default. - _request => ({}) as Result - ); - } - - /** - * Builds the context object for request handlers. Subclasses must override - * to return the appropriate context type (e.g., ServerContext adds HTTP request info). - */ - protected abstract buildContext(ctx: BaseContext, transportInfo?: MessageExtraInfo): ContextT; - - private async _oncancel(notification: CancelledNotification): Promise { - if (!notification.params.requestId) { - return; - } - // Handle request cancellation - const controller = this._requestHandlerAbortControllers.get(notification.params.requestId); - controller?.abort(notification.params.reason); - } - - private _setupTimeout( - messageId: number, - timeout: number, - maxTotalTimeout: number | undefined, - onTimeout: () => void, - resetTimeoutOnProgress: boolean = false - ) { - this._timeoutInfo.set(messageId, { - timeoutId: setTimeout(onTimeout, timeout), - startTime: Date.now(), - timeout, - maxTotalTimeout, - resetTimeoutOnProgress, - onTimeout - }); - } - - private _resetTimeout(messageId: number): boolean { - const info = this._timeoutInfo.get(messageId); - if (!info) return false; - - const totalElapsed = Date.now() - info.startTime; - if (info.maxTotalTimeout && totalElapsed >= info.maxTotalTimeout) { - this._timeoutInfo.delete(messageId); - throw new SdkError(SdkErrorCode.RequestTimeout, 'Maximum total timeout exceeded', { - maxTotalTimeout: info.maxTotalTimeout, - totalElapsed - }); - } - - clearTimeout(info.timeoutId); - info.timeoutId = setTimeout(info.onTimeout, info.timeout); - return true; - } - - private _cleanupTimeout(messageId: number) { - const info = this._timeoutInfo.get(messageId); - if (info) { - clearTimeout(info.timeoutId); - this._timeoutInfo.delete(messageId); - } - } - - /** - * Attaches to the given transport, starts it, and starts listening for messages. - * - * The caller assumes ownership of the {@linkcode Transport}, replacing any callbacks that have already been set, and expects that it is the only user of the {@linkcode Transport} instance going forward. - */ - async connect(transport: Transport): Promise { - this._transport = transport; - const _onclose = this.transport?.onclose; - this._transport.onclose = () => { - try { - _onclose?.(); - } finally { - this._onclose(); - } - }; - - const _onerror = this.transport?.onerror; - this._transport.onerror = (error: Error) => { - _onerror?.(error); - this._onerror(error); - }; - - const _onmessage = this._transport?.onmessage; - this._transport.onmessage = (message, extra) => { - _onmessage?.(message, extra); - if (isJSONRPCResultResponse(message) || isJSONRPCErrorResponse(message)) { - this._onresponse(message); - } else if (isJSONRPCRequest(message)) { - this._onrequest(message, extra); - } else if (isJSONRPCNotification(message)) { - this._onnotification(message); - } else { - this._onerror(new Error(`Unknown message type: ${JSON.stringify(message)}`)); - } - }; - - // Pass supported protocol versions to transport for header validation - transport.setSupportedProtocolVersions?.(this._supportedProtocolVersions); - - await this._transport.start(); - } - - private _onclose(): void { - const responseHandlers = this._responseHandlers; - this._responseHandlers = new Map(); - this._progressHandlers.clear(); - this._pendingDebouncedNotifications.clear(); - - for (const info of this._timeoutInfo.values()) { - clearTimeout(info.timeoutId); - } - this._timeoutInfo.clear(); - - const requestHandlerAbortControllers = this._requestHandlerAbortControllers; - this._requestHandlerAbortControllers = new Map(); - - const error = new SdkError(SdkErrorCode.ConnectionClosed, 'Connection closed'); - - this._transport = undefined; - - try { - this.onclose?.(); - } finally { - for (const handler of responseHandlers.values()) { - handler(error); - } - - for (const controller of requestHandlerAbortControllers.values()) { - controller.abort(error); - } - } - } - - private _onerror(error: Error): void { - this.onerror?.(error); - } - - private _onnotification(notification: JSONRPCNotification): void { - const handler = this._notificationHandlers.get(notification.method) ?? this.fallbackNotificationHandler; - - // Ignore notifications not being subscribed to. - if (handler === undefined) { - return; - } - - // Starting with Promise.resolve() puts any synchronous errors into the monad as well. - Promise.resolve() - .then(() => handler(notification)) - .catch(error => this._onerror(new Error(`Uncaught error in notification handler: ${error}`))); - } - - private _onrequest(request: JSONRPCRequest, extra?: MessageExtraInfo): void { - const handler = this._requestHandlers.get(request.method) ?? this.fallbackRequestHandler; - - // Capture the current transport at request time to ensure responses go to the correct client - const capturedTransport = this._transport; - - const sendNotification = (notification: Notification, options?: NotificationOptions) => - this.notification(notification, { ...options, relatedRequestId: request.id }); - const sendRequest = (r: Request, resultSchema: U, options?: RequestOptions) => - this._requestWithSchema(r, resultSchema, { ...options, relatedRequestId: request.id }); - - if (handler === undefined) { - const errorResponse: JSONRPCErrorResponse = { - jsonrpc: '2.0', - id: request.id, - error: { - code: ProtocolErrorCode.MethodNotFound, - message: 'Method not found' - } - }; - capturedTransport?.send(errorResponse).catch(error => this._onerror(new Error(`Failed to send an error response: ${error}`))); - return; - } - - const abortController = new AbortController(); - this._requestHandlerAbortControllers.set(request.id, abortController); - - const baseCtx: BaseContext = { - sessionId: capturedTransport?.sessionId, - mcpReq: { - id: request.id, - method: request.method, - _meta: request.params?._meta, - signal: abortController.signal, - // BaseContext.mcpReq.send is declared with two overloads (spec-method-keyed and explicit-schema). Arrow - // literals can't carry overload signatures, so the inferred single-signature type isn't assignable to - // that overloaded property type. The cast is sound: this impl dispatches both overload paths via the - // isStandardSchema guard, and sendRequest validates the result against the resolved schema either way. - send: ((r: Request, schemaOrOptions?: StandardSchemaV1 | RequestOptions, maybeOptions?: RequestOptions) => { - if (isStandardSchema(schemaOrOptions)) { - return sendRequest(r, schemaOrOptions, maybeOptions); - } - const resultSchema = getResultSchema(r.method); - if (!resultSchema) { - throw new TypeError( - `'${r.method}' is not a spec method; pass a result schema as the second argument to ctx.mcpReq.send().` - ); - } - return sendRequest(r, resultSchema, schemaOrOptions); - }) as BaseContext['mcpReq']['send'], - notify: sendNotification - }, - http: extra?.authInfo ? { authInfo: extra.authInfo } : undefined - }; - const ctx = this.buildContext(baseCtx, extra); - - // Starting with Promise.resolve() puts any synchronous errors into the monad as well. - Promise.resolve() - .then(() => handler(request, ctx)) - .then( - async result => { - if (abortController.signal.aborted) { - // Request was cancelled - return; - } - - const response: JSONRPCResponse = { - result, - jsonrpc: '2.0', - id: request.id - }; - await capturedTransport?.send(response); - }, - async error => { - if (abortController.signal.aborted) { - // Request was cancelled - return; - } - - const errorResponse: JSONRPCErrorResponse = { - jsonrpc: '2.0', - id: request.id, - error: { - code: Number.isSafeInteger(error['code']) ? error['code'] : ProtocolErrorCode.InternalError, - message: error.message ?? 'Internal error', - ...(error['data'] !== undefined && { data: error['data'] }) - } - }; - await capturedTransport?.send(errorResponse); - } - ) - .catch(error => this._onerror(new Error(`Failed to send response: ${error}`))) - .finally(() => { - if (this._requestHandlerAbortControllers.get(request.id) === abortController) { - this._requestHandlerAbortControllers.delete(request.id); - } - }); - } - - private _onprogress(notification: ProgressNotification): void { - const { progressToken, ...params } = notification.params; - const messageId = Number(progressToken); - - const handler = this._progressHandlers.get(messageId); - if (!handler) { - this._onerror(new Error(`Received a progress notification for an unknown token: ${JSON.stringify(notification)}`)); - return; - } - - const responseHandler = this._responseHandlers.get(messageId); - const timeoutInfo = this._timeoutInfo.get(messageId); - - if (timeoutInfo && responseHandler && timeoutInfo.resetTimeoutOnProgress) { - try { - this._resetTimeout(messageId); - } catch (error) { - // Clean up if maxTotalTimeout was exceeded - this._responseHandlers.delete(messageId); - this._progressHandlers.delete(messageId); - this._cleanupTimeout(messageId); - responseHandler(error as Error); - return; - } - } - - handler(params); - } - - private _onresponse(response: JSONRPCResponse | JSONRPCErrorResponse): void { - const messageId = Number(response.id); - - const handler = this._responseHandlers.get(messageId); - if (handler === undefined) { - this._onerror(new Error(`Received a response for an unknown message ID: ${JSON.stringify(response)}`)); - return; - } - - this._responseHandlers.delete(messageId); - this._cleanupTimeout(messageId); - this._progressHandlers.delete(messageId); - - if (isJSONRPCResultResponse(response)) { - handler(response); - } else { - const error = ProtocolError.fromError(response.error.code, response.error.message, response.error.data); - handler(error); - } - } - - get transport(): Transport | undefined { - return this._transport; - } - - /** - * Closes the connection. - */ - async close(): Promise { - await this._transport?.close(); - } - - /** - * A method to check if a capability is supported by the remote side, for the given method to be called. - * - * This should be implemented by subclasses. - */ - protected abstract assertCapabilityForMethod(method: RequestMethod | string): void; - - /** - * A method to check if a notification is supported by the local side, for the given method to be sent. - * - * This should be implemented by subclasses. - */ - protected abstract assertNotificationCapability(method: NotificationMethod | string): void; - - /** - * A method to check if a request handler is supported by the local side, for the given method to be handled. - * - * This should be implemented by subclasses. - */ - protected abstract assertRequestHandlerCapability(method: string): void; - - /** - * Sends a request and waits for a response. - * - * For spec methods the result schema is resolved automatically from the method name - * and the return type is method-keyed. For custom (non-spec) methods, pass a - * `resultSchema` as the second argument; the response is validated against it and - * the return type is inferred from the schema. - * - * Do not use this method to emit notifications! Use {@linkcode Protocol.notification | notification()} instead. - */ - request( - request: { method: M; params?: Record }, - options?: RequestOptions - ): Promise; - request( - request: Request, - resultSchema: T, - options?: RequestOptions - ): Promise>; - request(request: Request, schemaOrOptions?: StandardSchemaV1 | RequestOptions, maybeOptions?: RequestOptions): Promise { - if (isStandardSchema(schemaOrOptions)) { - return this._requestWithSchema(request, schemaOrOptions, maybeOptions); - } - const resultSchema = getResultSchema(request.method); - if (!resultSchema) { - throw new TypeError(`'${request.method}' is not a spec method; pass a result schema as the second argument to request().`); - } - return this._requestWithSchema(request, resultSchema, schemaOrOptions); - } - - /** - * Sends a request and waits for a response, using the provided schema for validation. - * - * This is the internal implementation used by SDK methods that need to specify - * a particular result schema (e.g., for compatibility schemas). - */ - protected _requestWithSchema( - request: Request, - resultSchema: T, - options?: RequestOptions - ): Promise> { - const { relatedRequestId, resumptionToken, onresumptiontoken } = options ?? {}; - - let onAbort: (() => void) | undefined; - let cleanupMessageId: number | undefined; - - // Send the request - return new Promise>((resolve, reject) => { - const earlyReject = (error: unknown) => { - reject(error); - }; - - if (!this._transport) { - earlyReject(new Error('Not connected')); - return; - } - - if (this._options?.enforceStrictCapabilities === true) { - try { - this.assertCapabilityForMethod(request.method); - } catch (error) { - earlyReject(error); - return; - } - } - - options?.signal?.throwIfAborted(); - - const messageId = this._requestMessageId++; - cleanupMessageId = messageId; - const jsonrpcRequest: JSONRPCRequest = { - ...request, - jsonrpc: '2.0', - id: messageId - }; - - if (options?.onprogress) { - this._progressHandlers.set(messageId, options.onprogress); - jsonrpcRequest.params = { - ...request.params, - _meta: { - ...request.params?._meta, - progressToken: messageId - } - }; - } - - let responseReceived = false; - - const cancel = (reason: unknown) => { - if (responseReceived) { - return; - } - this._progressHandlers.delete(messageId); - - this._transport - ?.send( - { - jsonrpc: '2.0', - method: 'notifications/cancelled', - params: { - requestId: messageId, - reason: String(reason) - } - }, - { relatedRequestId, resumptionToken, onresumptiontoken } - ) - .catch(error => this._onerror(new Error(`Failed to send cancellation: ${error}`))); - - // Wrap the reason in an SdkError if it isn't already - const error = reason instanceof SdkError ? reason : new SdkError(SdkErrorCode.RequestTimeout, String(reason)); - reject(error); - }; - - this._responseHandlers.set(messageId, response => { - if (options?.signal?.aborted) { - return; - } - responseReceived = true; - - if (response instanceof Error) { - return reject(response); - } - - validateStandardSchema(resultSchema, response.result).then(parseResult => { - if (parseResult.success) { - resolve(parseResult.data); - } else { - reject(new SdkError(SdkErrorCode.InvalidResult, `Invalid result for ${request.method}: ${parseResult.error}`)); - } - }, reject); - }); - - onAbort = () => cancel(options?.signal?.reason); - options?.signal?.addEventListener('abort', onAbort, { once: true }); - - const timeout = options?.timeout ?? DEFAULT_REQUEST_TIMEOUT_MSEC; - const timeoutHandler = () => cancel(new SdkError(SdkErrorCode.RequestTimeout, 'Request timed out', { timeout })); - - this._setupTimeout(messageId, timeout, options?.maxTotalTimeout, timeoutHandler, options?.resetTimeoutOnProgress ?? false); - - this._transport.send(jsonrpcRequest, { relatedRequestId, resumptionToken, onresumptiontoken }).catch(error => { - this._progressHandlers.delete(messageId); - reject(error); - }); - }).finally(() => { - // Per-request cleanup that must run on every exit path. Consolidated - // here so new exit paths added to the promise body can't forget it. - // _progressHandlers is NOT cleaned up here: _onresponse deletes it - // on resolution, and error paths above delete it inline. - if (onAbort) { - options?.signal?.removeEventListener('abort', onAbort); - } - if (cleanupMessageId !== undefined) { - this._responseHandlers.delete(cleanupMessageId); - this._cleanupTimeout(cleanupMessageId); - } - }); - } - - /** - * Emits a notification, which is a one-way message that does not expect a response. - */ - async notification(notification: Notification, options?: NotificationOptions): Promise { - if (!this._transport) { - throw new SdkError(SdkErrorCode.NotConnected, 'Not connected'); - } - - this.assertNotificationCapability(notification.method); - - const jsonrpcNotification: JSONRPCNotification = { jsonrpc: '2.0', ...notification }; - - const debouncedMethods = this._options?.debouncedNotificationMethods ?? []; - // A notification can only be debounced if it's in the list AND it's "simple" - // (i.e., has no parameters and no related request ID that could be lost). - const canDebounce = debouncedMethods.includes(notification.method) && !notification.params && !options?.relatedRequestId; - - if (canDebounce) { - // If a notification of this type is already scheduled, do nothing. - if (this._pendingDebouncedNotifications.has(notification.method)) { - return; - } - - // Mark this notification type as pending. - this._pendingDebouncedNotifications.add(notification.method); - - // Schedule the actual send to happen in the next microtask. - // This allows all synchronous calls in the current event loop tick to be coalesced. - Promise.resolve().then(() => { - // Un-mark the notification so the next one can be scheduled. - this._pendingDebouncedNotifications.delete(notification.method); - - // SAFETY CHECK: If the connection was closed while this was pending, abort. - if (!this._transport) { - return; - } - - // Send the notification, but don't await it here to avoid blocking. - // Handle potential errors with a .catch(). - this._transport?.send(jsonrpcNotification, options).catch(error => this._onerror(error)); - }); - - // Return immediately. - return; - } - - await this._transport.send(jsonrpcNotification, options); - } - - /** - * Registers a handler to invoke when this protocol object receives a request with the given method. - * - * Note that this will replace any previous request handler for the same method. - * - * For spec methods, pass `(method, handler)`; the request is parsed with the spec - * schema and the handler receives the typed `Request`. For custom (non-spec) - * methods, pass `(method, schemas, handler)`; `params` are validated against - * `schemas.params` and the handler receives the parsed params object directly. - * Supplying `schemas.result` types the handler's return value. - * - * @example Custom request method - * ```ts source="./protocol.examples.ts#Protocol_setRequestHandler_customMethod" - * const SearchParams = z.object({ query: z.string(), limit: z.number().optional() }); - * const SearchResult = z.object({ hits: z.array(z.string()) }); - * - * protocol.setRequestHandler('acme/search', { params: SearchParams, result: SearchResult }, async (params, _ctx) => { - * return { hits: [`result for ${params.query}`] }; - * }); - * ``` - */ - setRequestHandler( - method: M, - handler: (request: RequestTypeMap[M], ctx: ContextT) => ResultTypeMap[M] | Promise - ): void; - setRequestHandler

( - method: string, - schemas: { params: P; result?: R }, - handler: (params: StandardSchemaV1.InferOutput

, ctx: ContextT) => InferHandlerResult | Promise> - ): void; - setRequestHandler( - method: string, - schemasOrHandler: RequestHandlerSchemas | ((request: unknown, ctx: ContextT) => Result | Promise), - maybeHandler?: (params: unknown, ctx: ContextT) => Result | Promise - ): void { - this.assertRequestHandlerCapability(method); - - let stored: (request: JSONRPCRequest, ctx: ContextT) => Promise; - - if (typeof schemasOrHandler === 'function') { - const schema = getRequestSchema(method); - if (!schema) { - throw new TypeError( - `'${method}' is not a spec request method; pass schemas as the second argument to setRequestHandler().` - ); - } - stored = (request, ctx) => Promise.resolve(schemasOrHandler(schema.parse(request), ctx)); - } else if (maybeHandler) { - stored = async (request, ctx) => { - const userParams = { ...request.params }; - delete userParams._meta; - const parsed = await validateStandardSchema(schemasOrHandler.params, userParams); - if (!parsed.success) { - throw new ProtocolError(ProtocolErrorCode.InvalidParams, `Invalid params for ${method}: ${parsed.error}`); - } - return maybeHandler(parsed.data, ctx); - }; - } else { - throw new TypeError('setRequestHandler: handler is required'); - } - - this._requestHandlers.set(method, this._wrapHandler(method, stored)); - } - - /** - * Hook for subclasses to wrap a registered request handler with role-specific - * validation or behavior (e.g. `Server` validates `tools/call` results, `Client` - * validates `elicitation/create` mode and result). Runs for both the 2-arg and - * 3-arg registration paths. The default implementation is identity. - * - * Subclasses overriding this hook avoid redeclaring `setRequestHandler`'s overload set. - */ - protected _wrapHandler( - _method: string, - handler: (request: JSONRPCRequest, ctx: ContextT) => Promise - ): (request: JSONRPCRequest, ctx: ContextT) => Promise { - return handler; - } - - /** - * Removes the request handler for the given method. - */ - removeRequestHandler(method: RequestMethod | string): void { - this._requestHandlers.delete(method); - } - - /** - * Asserts that a request handler has not already been set for the given method, in preparation for a new one being automatically installed. - */ - assertCanSetRequestHandler(method: RequestMethod | string): void { - if (this._requestHandlers.has(method)) { - throw new Error(`A request handler for ${method} already exists, which would be overridden`); - } - } - - /** - * Registers a handler to invoke when this protocol object receives a notification with the given method. - * - * Note that this will replace any previous notification handler for the same method. - * - * For spec methods, pass `(method, handler)`; the notification is parsed with the - * spec schema. For custom (non-spec) methods, pass `(method, schemas, handler)`; - * `params` are validated against `schemas.params` and the handler receives the - * parsed params object directly. The raw notification is passed as the second - * argument; `_meta` is recoverable via `notification.params?._meta`. - */ - setNotificationHandler( - method: M, - handler: (notification: NotificationTypeMap[M]) => void | Promise - ): void; - setNotificationHandler

( - method: string, - schemas: { params: P }, - handler: (params: StandardSchemaV1.InferOutput

, notification: Notification) => void | Promise - ): void; - setNotificationHandler( - method: string, - schemasOrHandler: { params: StandardSchemaV1 } | ((notification: unknown) => void | Promise), - maybeHandler?: (params: unknown, notification: Notification) => void | Promise - ): void { - if (typeof schemasOrHandler === 'function') { - const schema = getNotificationSchema(method); - if (!schema) { - throw new TypeError( - `'${method}' is not a spec notification method; pass schemas as the second argument to setNotificationHandler().` - ); - } - this._notificationHandlers.set(method, notification => Promise.resolve(schemasOrHandler(schema.parse(notification)))); - return; - } - - if (!maybeHandler) { - throw new TypeError('setNotificationHandler: handler is required'); - } - this._notificationHandlers.set(method, async notification => { - const userParams = { ...notification.params }; - delete userParams._meta; - const parsed = await validateStandardSchema(schemasOrHandler.params, userParams); - if (!parsed.success) { - throw new ProtocolError(ProtocolErrorCode.InvalidParams, `Invalid params for notification ${method}: ${parsed.error}`); - } - await maybeHandler(parsed.data, notification); - }); - } - - /** - * Removes the notification handler for the given method. - */ - removeNotificationHandler(method: NotificationMethod | string): void { - this._notificationHandlers.delete(method); - } -} - -/** - * Schema bundle accepted by {@linkcode Protocol.setRequestHandler | setRequestHandler}'s 3-arg form. - * - * `params` is required and validates the inbound `request.params`. `result` is optional; - * when supplied it types the handler's return value (no runtime validation is performed - * on the result). - */ -export interface RequestHandlerSchemas< - P extends StandardSchemaV1 = StandardSchemaV1, - R extends StandardSchemaV1 | undefined = StandardSchemaV1 | undefined -> { - params: P; - result?: R; -} - -type InferHandlerResult = R extends StandardSchemaV1 ? StandardSchemaV1.InferOutput : Result; - -function isPlainObject(value: unknown): value is Record { - return value !== null && typeof value === 'object' && !Array.isArray(value); -} - -export function mergeCapabilities(base: ServerCapabilities, additional: Partial): ServerCapabilities; -export function mergeCapabilities(base: ClientCapabilities, additional: Partial): ClientCapabilities; -export function mergeCapabilities(base: T, additional: Partial): T { - const result: T = { ...base }; - for (const key in additional) { - const k = key as keyof T; - const addValue = additional[k]; - if (addValue === undefined) continue; - const baseValue = result[k]; - result[k] = - isPlainObject(baseValue) && isPlainObject(addValue) - ? ({ ...(baseValue as Record), ...(addValue as Record) } as T[typeof k]) - : (addValue as T[typeof k]); - } - return result; -} diff --git a/packages/core/src/shared/stdio.ts b/packages/core/src/shared/stdio.ts deleted file mode 100644 index 8bd794b87b..0000000000 --- a/packages/core/src/shared/stdio.ts +++ /dev/null @@ -1,62 +0,0 @@ -import type { JSONRPCMessage } from '../types/index'; -import { JSONRPCMessageSchema } from '../types/index'; - -export const STDIO_DEFAULT_MAX_BUFFER_SIZE = 10 * 1024 * 1024; - -/** - * Buffers a continuous stdio stream into discrete JSON-RPC messages. - */ -export class ReadBuffer { - private _buffer?: Buffer; - private _maxBufferSize: number; - - constructor(options?: { maxBufferSize?: number }) { - this._maxBufferSize = options?.maxBufferSize ?? STDIO_DEFAULT_MAX_BUFFER_SIZE; - } - - append(chunk: Buffer): void { - const newSize = (this._buffer?.length ?? 0) + chunk.length; - if (newSize > this._maxBufferSize) { - this.clear(); - throw new Error(`ReadBuffer exceeded maximum size of ${this._maxBufferSize} bytes`); - } - this._buffer = this._buffer ? Buffer.concat([this._buffer, chunk]) : chunk; - } - - readMessage(): JSONRPCMessage | null { - while (this._buffer) { - const index = this._buffer.indexOf('\n'); - if (index === -1) { - return null; - } - - const line = this._buffer.toString('utf8', 0, index).replace(/\r$/, ''); - this._buffer = this._buffer.subarray(index + 1); - - try { - return deserializeMessage(line); - } catch (error) { - // Skip non-JSON lines (e.g., debug output from hot-reload tools like - // tsx or nodemon that write to stdout). Schema validation errors still - // throw so malformed-but-valid-JSON messages surface via onerror. - if (error instanceof SyntaxError) { - continue; - } - throw error; - } - } - return null; - } - - clear(): void { - this._buffer = undefined; - } -} - -export function deserializeMessage(line: string): JSONRPCMessage { - return JSONRPCMessageSchema.parse(JSON.parse(line)); -} - -export function serializeMessage(message: JSONRPCMessage): string { - return JSON.stringify(message) + '\n'; -} diff --git a/packages/core/src/shared/toolNameValidation.ts b/packages/core/src/shared/toolNameValidation.ts deleted file mode 100644 index 41bc44953d..0000000000 --- a/packages/core/src/shared/toolNameValidation.ts +++ /dev/null @@ -1,116 +0,0 @@ -/** - * Tool name validation utilities according to SEP: Specify Format for Tool Names - * - * Tool names SHOULD be between 1 and 128 characters in length (inclusive). - * Tool names are case-sensitive. - * Allowed characters: uppercase and lowercase ASCII letters (`A-Z`, `a-z`), digits - * (`0-9`), underscore (`_`), dash (`-`), and dot (`.`). - * Tool names SHOULD NOT contain spaces, commas, or other special characters. - * - * @see {@link https://github.com/modelcontextprotocol/modelcontextprotocol/issues/986 | SEP-986: Specify Format for Tool Names} - */ - -/** - * Regular expression for valid tool names according to SEP-986 specification - */ -const TOOL_NAME_REGEX = /^[A-Za-z0-9._-]{1,128}$/; - -/** - * Validates a tool name according to the SEP specification - * @param name - The tool name to validate - * @returns An object containing validation result and any warnings - */ -export function validateToolName(name: string): { - isValid: boolean; - warnings: string[]; -} { - const warnings: string[] = []; - - // Check length - if (name.length === 0) { - return { - isValid: false, - warnings: ['Tool name cannot be empty'] - }; - } - - if (name.length > 128) { - return { - isValid: false, - warnings: [`Tool name exceeds maximum length of 128 characters (current: ${name.length})`] - }; - } - - // Check for specific problematic patterns (these are warnings, not validation failures) - if (name.includes(' ')) { - warnings.push('Tool name contains spaces, which may cause parsing issues'); - } - - if (name.includes(',')) { - warnings.push('Tool name contains commas, which may cause parsing issues'); - } - - // Check for potentially confusing patterns (leading/trailing dashes, dots, slashes) - if (name.startsWith('-') || name.endsWith('-')) { - warnings.push('Tool name starts or ends with a dash, which may cause parsing issues in some contexts'); - } - - if (name.startsWith('.') || name.endsWith('.')) { - warnings.push('Tool name starts or ends with a dot, which may cause parsing issues in some contexts'); - } - - // Check for invalid characters - if (!TOOL_NAME_REGEX.test(name)) { - const invalidChars = [...name] - .filter(char => !/[A-Za-z0-9._-]/.test(char)) - .filter((char, index, arr) => arr.indexOf(char) === index); // Remove duplicates - - warnings.push( - `Tool name contains invalid characters: ${invalidChars.map(c => `"${c}"`).join(', ')}`, - 'Allowed characters are: A-Z, a-z, 0-9, underscore (_), dash (-), and dot (.)' - ); - - return { - isValid: false, - warnings - }; - } - - return { - isValid: true, - warnings - }; -} - -/** - * Issues warnings for non-conforming tool names - * @param name - The tool name that triggered the warnings - * @param warnings - Array of warning messages - */ -export function issueToolNameWarning(name: string, warnings: string[]): void { - if (warnings.length > 0) { - console.warn(`Tool name validation warning for "${name}":`); - for (const warning of warnings) { - console.warn(` - ${warning}`); - } - console.warn('Tool registration will proceed, but this may cause compatibility issues.'); - console.warn('Consider updating the tool name to conform to the MCP tool naming standard.'); - console.warn( - 'See SEP: Specify Format for Tool Names (https://github.com/modelcontextprotocol/modelcontextprotocol/issues/986) for more details.' - ); - } -} - -/** - * Validates a tool name and issues warnings for non-conforming names - * @param name - The tool name to validate - * @returns `true` if the name is valid, `false` otherwise - */ -export function validateAndWarnToolName(name: string): boolean { - const result = validateToolName(name); - - // Always issue warnings for any validation issues (both invalid names and warnings) - issueToolNameWarning(name, result.warnings); - - return result.isValid; -} diff --git a/packages/core/src/shared/transport.ts b/packages/core/src/shared/transport.ts deleted file mode 100644 index ddca2f7992..0000000000 --- a/packages/core/src/shared/transport.ts +++ /dev/null @@ -1,134 +0,0 @@ -import type { JSONRPCMessage, MessageExtraInfo, RequestId } from '../types/index'; - -export type FetchLike = (url: string | URL, init?: RequestInit) => Promise; - -/** - * Normalizes `HeadersInit` to a plain `Record` for manipulation. - * Handles `Headers` objects, arrays of tuples, and plain objects. - */ -export function normalizeHeaders(headers: RequestInit['headers'] | undefined): Record { - if (!headers) return {}; - - if (headers instanceof Headers) { - return Object.fromEntries(headers.entries()); - } - - if (Array.isArray(headers)) { - return Object.fromEntries(headers); - } - - return { ...(headers as Record) }; -} - -/** - * Creates a fetch function that includes base `RequestInit` options. - * This ensures requests inherit settings like credentials, mode, headers, etc. from the base init. - * - * @param baseFetch - The base fetch function to wrap (defaults to global `fetch`) - * @param baseInit - The base `RequestInit` to merge with each request - * @returns A wrapped fetch function that merges base options with call-specific options - */ -export function createFetchWithInit(baseFetch: FetchLike = fetch, baseInit?: RequestInit): FetchLike { - if (!baseInit) { - return baseFetch; - } - - // Return a wrapped fetch that merges base RequestInit with call-specific init - return async (url: string | URL, init?: RequestInit): Promise => { - const mergedInit: RequestInit = { - ...baseInit, - ...init, - // Headers need special handling - merge instead of replace - headers: init?.headers ? { ...normalizeHeaders(baseInit.headers), ...normalizeHeaders(init.headers) } : baseInit.headers - }; - return baseFetch(url, mergedInit); - }; -} - -/** - * Options for sending a JSON-RPC message. - */ -export type TransportSendOptions = { - /** - * If present, `relatedRequestId` is used to indicate to the transport which incoming request to associate this outgoing message with. - */ - relatedRequestId?: RequestId | undefined; - - /** - * The resumption token used to continue long-running requests that were interrupted. - * - * This allows clients to reconnect and continue from where they left off, if supported by the transport. - */ - resumptionToken?: string | undefined; - - /** - * A callback that is invoked when the resumption token changes, if supported by the transport. - * - * This allows clients to persist the latest token for potential reconnection. - */ - onresumptiontoken?: ((token: string) => void) | undefined; -}; -/** - * Describes the minimal contract for an MCP transport that a client or server can communicate over. - */ -export interface Transport { - /** - * Starts processing messages on the transport, including any connection steps that might need to be taken. - * - * This method should only be called after callbacks are installed, or else messages may be lost. - * - * NOTE: This method should not be called explicitly when using {@linkcode @modelcontextprotocol/client!client/client.Client | Client} or {@linkcode @modelcontextprotocol/server!server/server.Server | Server} classes, as they will implicitly call {@linkcode Transport.start | start()}. - */ - start(): Promise; - - /** - * Sends a JSON-RPC message (request or response). - * - * If present, `relatedRequestId` is used to indicate to the transport which incoming request to associate this outgoing message with. - */ - send(message: JSONRPCMessage, options?: TransportSendOptions): Promise; - - /** - * Closes the connection. - */ - close(): Promise; - - /** - * Callback for when the connection is closed for any reason. - * - * This should be invoked when {@linkcode Transport.close | close()} is called as well. - */ - onclose?: (() => void) | undefined; - - /** - * Callback for when an error occurs. - * - * Note that errors are not necessarily fatal; they are used for reporting any kind of exceptional condition out of band. - */ - onerror?: ((error: Error) => void) | undefined; - - /** - * Callback for when a message (request or response) is received over the connection. - * - * Includes the {@linkcode MessageExtraInfo.request | request} and {@linkcode MessageExtraInfo.authInfo | authInfo} if the transport is authenticated. - * - * The {@linkcode MessageExtraInfo.request | request} can be used to get the original request information (headers, etc.) - */ - onmessage?: ((message: T, extra?: MessageExtraInfo) => void) | undefined; - - /** - * The session ID generated for this connection. - */ - sessionId?: string | undefined; - - /** - * Sets the protocol version used for the connection (called when the initialize response is received). - */ - setProtocolVersion?: ((version: string) => void) | undefined; - - /** - * Sets the supported protocol versions for header validation (called during connect). - * This allows the server to pass its supported versions to the transport. - */ - setSupportedProtocolVersions?: ((versions: string[]) => void) | undefined; -} diff --git a/packages/core/src/shared/uriTemplate.ts b/packages/core/src/shared/uriTemplate.ts deleted file mode 100644 index 5ffe213acd..0000000000 --- a/packages/core/src/shared/uriTemplate.ts +++ /dev/null @@ -1,290 +0,0 @@ -// Claude-authored implementation of RFC 6570 URI Templates - -export type Variables = Record; - -const MAX_TEMPLATE_LENGTH = 1_000_000; // 1MB -const MAX_VARIABLE_LENGTH = 1_000_000; // 1MB -const MAX_TEMPLATE_EXPRESSIONS = 10_000; -const MAX_REGEX_LENGTH = 1_000_000; // 1MB - -export class UriTemplate { - /** - * Returns true if the given string contains any URI template expressions. - * A template expression is a sequence of characters enclosed in curly braces, - * like `{foo}` or `{?bar}`. - */ - static isTemplate(str: string): boolean { - // Look for any sequence of characters between curly braces - // that isn't just whitespace - return /\{[^}\s]+\}/.test(str); - } - - private static validateLength(str: string, max: number, context: string): void { - if (str.length > max) { - throw new Error(`${context} exceeds maximum length of ${max} characters (got ${str.length})`); - } - } - private readonly template: string; - private readonly parts: Array; - - get variableNames(): string[] { - return this.parts.flatMap(part => (typeof part === 'string' ? [] : part.names)); - } - - constructor(template: string) { - UriTemplate.validateLength(template, MAX_TEMPLATE_LENGTH, 'Template'); - this.template = template; - this.parts = this.parse(template); - } - - toString(): string { - return this.template; - } - - private parse(template: string): Array { - const parts: Array = []; - let currentText = ''; - let i = 0; - let expressionCount = 0; - - while (i < template.length) { - if (template[i] === '{') { - if (currentText) { - parts.push(currentText); - currentText = ''; - } - const end = template.indexOf('}', i); - if (end === -1) throw new Error('Unclosed template expression'); - - expressionCount++; - if (expressionCount > MAX_TEMPLATE_EXPRESSIONS) { - throw new Error(`Template contains too many expressions (max ${MAX_TEMPLATE_EXPRESSIONS})`); - } - - const expr = template.slice(i + 1, end); - const operator = this.getOperator(expr); - const exploded = expr.includes('*'); - const names = this.getNames(expr); - const name = names[0]!; - - // Validate variable name length - for (const name of names) { - UriTemplate.validateLength(name, MAX_VARIABLE_LENGTH, 'Variable name'); - } - - parts.push({ name, operator, names, exploded }); - i = end + 1; - } else { - currentText += template[i]; - i++; - } - } - - if (currentText) { - parts.push(currentText); - } - - return parts; - } - - private getOperator(expr: string): string { - const operators = ['+', '#', '.', '/', '?', '&']; - return operators.find(op => expr.startsWith(op)) || ''; - } - - private getNames(expr: string): string[] { - const operator = this.getOperator(expr); - return expr - .slice(operator.length) - .split(',') - .map(name => name.replace('*', '').trim()) - .filter(name => name.length > 0); - } - - private encodeValue(value: string, operator: string): string { - UriTemplate.validateLength(value, MAX_VARIABLE_LENGTH, 'Variable value'); - if (operator === '+' || operator === '#') { - return encodeURI(value); - } - return encodeURIComponent(value); - } - - private expandPart( - part: { - name: string; - operator: string; - names: string[]; - exploded: boolean; - }, - variables: Variables - ): string { - if (part.operator === '?' || part.operator === '&') { - const pairs = part.names - .map(name => { - const value = variables[name]; - if (value === undefined) return ''; - const encoded = Array.isArray(value) - ? value.map(v => this.encodeValue(v, part.operator)).join(',') - : this.encodeValue(value.toString(), part.operator); - return `${name}=${encoded}`; - }) - .filter(pair => pair.length > 0); - - if (pairs.length === 0) return ''; - const separator = part.operator === '?' ? '?' : '&'; - return separator + pairs.join('&'); - } - - if (part.names.length > 1) { - const values = part.names.map(name => variables[name]).filter(v => v !== undefined); - if (values.length === 0) return ''; - return values.map(v => (Array.isArray(v) ? v[0] : v)).join(','); - } - - const value = variables[part.name]; - if (value === undefined) return ''; - - const values = Array.isArray(value) ? value : [value]; - const encoded = values.map(v => this.encodeValue(v, part.operator)); - - switch (part.operator) { - case '': { - return encoded.join(','); - } - case '+': { - return encoded.join(','); - } - case '#': { - return '#' + encoded.join(','); - } - case '.': { - return '.' + encoded.join('.'); - } - case '/': { - return '/' + encoded.join('/'); - } - default: { - return encoded.join(','); - } - } - } - - expand(variables: Variables): string { - let result = ''; - let hasQueryParam = false; - - for (const part of this.parts) { - if (typeof part === 'string') { - result += part; - continue; - } - - const expanded = this.expandPart(part, variables); - if (!expanded) continue; - - // Convert ? to & if we already have a query parameter - result += (part.operator === '?' || part.operator === '&') && hasQueryParam ? expanded.replace('?', '&') : expanded; - - if (part.operator === '?' || part.operator === '&') { - hasQueryParam = true; - } - } - - return result; - } - - private escapeRegExp(str: string): string { - return str.replaceAll(/[.*+?^${}()|[\]\\]/g, String.raw`\$&`); - } - - private partToRegExp(part: { - name: string; - operator: string; - names: string[]; - exploded: boolean; - }): Array<{ pattern: string; name: string }> { - const patterns: Array<{ pattern: string; name: string }> = []; - - // Validate variable name length for matching - for (const name of part.names) { - UriTemplate.validateLength(name, MAX_VARIABLE_LENGTH, 'Variable name'); - } - - if (part.operator === '?' || part.operator === '&') { - for (let i = 0; i < part.names.length; i++) { - const name = part.names[i]!; - const prefix = i === 0 ? '\\' + part.operator : '&'; - patterns.push({ - pattern: prefix + this.escapeRegExp(name) + '=([^&]+)', - name - }); - } - return patterns; - } - - let pattern: string; - const name = part.name; - - switch (part.operator) { - case '': { - pattern = part.exploded ? '([^/,]+(?:,[^/,]+)*)' : '([^/,]+)'; - break; - } - case '+': - case '#': { - pattern = '(.+)'; - break; - } - case '.': { - pattern = String.raw`\.([^/,]+)`; - break; - } - case '/': { - pattern = '/' + (part.exploded ? '([^/,]+(?:,[^/,]+)*)' : '([^/,]+)'); - break; - } - default: { - pattern = '([^/]+)'; - } - } - - patterns.push({ pattern, name }); - return patterns; - } - - match(uri: string): Variables | null { - UriTemplate.validateLength(uri, MAX_TEMPLATE_LENGTH, 'URI'); - let pattern = '^'; - const names: Array<{ name: string; exploded: boolean }> = []; - - for (const part of this.parts) { - if (typeof part === 'string') { - pattern += this.escapeRegExp(part); - } else { - const patterns = this.partToRegExp(part); - for (const { pattern: partPattern, name } of patterns) { - pattern += partPattern; - names.push({ name, exploded: part.exploded }); - } - } - } - - pattern += '$'; - UriTemplate.validateLength(pattern, MAX_REGEX_LENGTH, 'Generated regex pattern'); - const regex = new RegExp(pattern); - const match = uri.match(regex); - - if (!match) return null; - - const result: Variables = {}; - for (const [i, name_] of names.entries()) { - const { name, exploded } = name_!; - const value = match[i + 1]!; - const cleanName = name.replace('*', ''); - - result[cleanName] = exploded && value.includes(',') ? value.split(',') : value; - } - - return result; - } -} diff --git a/packages/core/src/types/constants.ts b/packages/core/src/types/constants.ts deleted file mode 100644 index 018f9ecb51..0000000000 --- a/packages/core/src/types/constants.ts +++ /dev/null @@ -1,86 +0,0 @@ -export const LATEST_PROTOCOL_VERSION = '2025-11-25'; -export const DEFAULT_NEGOTIATED_PROTOCOL_VERSION = '2025-03-26'; -export const SUPPORTED_PROTOCOL_VERSIONS = [LATEST_PROTOCOL_VERSION, '2025-06-18', '2025-03-26', '2024-11-05', '2024-10-07']; - -export const RELATED_TASK_META_KEY = 'io.modelcontextprotocol/related-task'; - -/* Reserved `_meta` keys for the per-request envelope (protocol revision 2026-07-28) */ - -/** - * `_meta` key carrying the MCP protocol version governing a request. - * - * For the HTTP transport, the value must match the `MCP-Protocol-Version` header. - */ -export const PROTOCOL_VERSION_META_KEY = 'io.modelcontextprotocol/protocolVersion'; - -/** - * `_meta` key identifying the client software making a request. - */ -export const CLIENT_INFO_META_KEY = 'io.modelcontextprotocol/clientInfo'; - -/** - * `_meta` key carrying the client's capabilities for a request. - * - * Capabilities are declared per request rather than once at initialization; - * servers must not infer capabilities from prior requests. - */ -export const CLIENT_CAPABILITIES_META_KEY = 'io.modelcontextprotocol/clientCapabilities'; - -/** - * `_meta` key carrying the desired log level for a request. - * - * When absent, the server must not send `notifications/message` notifications - * for the request. - * - * @deprecated Deprecated as of protocol version 2026-07-28 (SEP-2577); remains - * in the specification for at least twelve months. - */ -export const LOG_LEVEL_META_KEY = 'io.modelcontextprotocol/logLevel'; - -/* - * Reserved `_meta` keys for distributed trace context propagation (SEP-414). - * - * These unprefixed keys are reserved by the MCP specification as an explicit - * exception to the `_meta` key prefix rule. The SDK does not interpret them; - * they pass through `_meta` untouched for OpenTelemetry-style propagation. - */ - -/** - * `_meta` key carrying W3C Trace Context for distributed tracing (SEP-414). - * - * When present, the value MUST follow the W3C `traceparent` header format, - * e.g. `00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01`. - * - * @see https://www.w3.org/TR/trace-context/#traceparent-header - */ -export const TRACEPARENT_META_KEY = 'traceparent'; - -/** - * `_meta` key carrying vendor-specific trace state for distributed tracing (SEP-414). - * - * When present, the value MUST follow the W3C `tracestate` header format, - * e.g. `vendor1=value1,vendor2=value2`. - * - * @see https://www.w3.org/TR/trace-context/#tracestate-header - */ -export const TRACESTATE_META_KEY = 'tracestate'; - -/** - * `_meta` key carrying cross-cutting propagation values for distributed tracing (SEP-414). - * - * When present, the value MUST follow the W3C Baggage header format, - * e.g. `userId=alice,serverRegion=us-east-1`. - * - * @see https://www.w3.org/TR/baggage/ - */ -export const BAGGAGE_META_KEY = 'baggage'; - -/* JSON-RPC types */ -export const JSONRPC_VERSION = '2.0'; - -/* Standard JSON-RPC error code constants */ -export const PARSE_ERROR = -32_700; -export const INVALID_REQUEST = -32_600; -export const METHOD_NOT_FOUND = -32_601; -export const INVALID_PARAMS = -32_602; -export const INTERNAL_ERROR = -32_603; diff --git a/packages/core/src/types/enums.ts b/packages/core/src/types/enums.ts deleted file mode 100644 index 0e3b65f9f0..0000000000 --- a/packages/core/src/types/enums.ts +++ /dev/null @@ -1,26 +0,0 @@ -/** - * Error codes for protocol errors that cross the wire as JSON-RPC error responses. - * These follow the JSON-RPC specification and MCP-specific extensions. - */ -export enum ProtocolErrorCode { - // Standard JSON-RPC error codes - ParseError = -32_700, - InvalidRequest = -32_600, - MethodNotFound = -32_601, - InvalidParams = -32_602, - InternalError = -32_603, - - // MCP-specific error codes - ResourceNotFound = -32_002, - /** - * Processing the request requires a capability the client did not declare - * in the request's `clientCapabilities` (protocol revision 2026-07-28). - */ - MissingRequiredClientCapability = -32_003, - /** - * The request's protocol version is unknown to the server or unsupported - * by it (protocol revision 2026-07-28). - */ - UnsupportedProtocolVersion = -32_004, - UrlElicitationRequired = -32_042 -} diff --git a/packages/core/src/types/errors.ts b/packages/core/src/types/errors.ts deleted file mode 100644 index 3d3654d46f..0000000000 --- a/packages/core/src/types/errors.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { ProtocolErrorCode } from './enums'; -import type { ElicitRequestURLParams, UnsupportedProtocolVersionErrorData } from './types'; - -/** - * Protocol errors are JSON-RPC errors that cross the wire as error responses. - * They use numeric error codes from the {@linkcode ProtocolErrorCode} enum. - */ -export class ProtocolError extends Error { - constructor( - public readonly code: number, - message: string, - public readonly data?: unknown - ) { - super(message); - this.name = 'ProtocolError'; - } - - /** - * Factory method to create the appropriate error type based on the error code and data - */ - static fromError(code: number, message: string, data?: unknown): ProtocolError { - // Check for specific error types - if (code === ProtocolErrorCode.UrlElicitationRequired && data) { - const errorData = data as { elicitations?: unknown[] }; - if (errorData.elicitations) { - return new UrlElicitationRequiredError(errorData.elicitations as ElicitRequestURLParams[], message); - } - } - - if (code === ProtocolErrorCode.UnsupportedProtocolVersion && data) { - const errorData = data as Partial; - if (Array.isArray(errorData.supported) && typeof errorData.requested === 'string') { - return new UnsupportedProtocolVersionError({ supported: errorData.supported, requested: errorData.requested }, message); - } - } - - // Default to generic ProtocolError - return new ProtocolError(code, message, data); - } -} - -/** - * Specialized error type when a tool requires a URL mode elicitation. - * This makes it nicer for the client to handle since there is specific data to work with instead of just a code to check against. - */ -export class UrlElicitationRequiredError extends ProtocolError { - constructor(elicitations: ElicitRequestURLParams[], message: string = `URL elicitation${elicitations.length > 1 ? 's' : ''} required`) { - super(ProtocolErrorCode.UrlElicitationRequired, message, { - elicitations: elicitations - }); - } - - get elicitations(): ElicitRequestURLParams[] { - return (this.data as { elicitations: ElicitRequestURLParams[] })?.elicitations ?? []; - } -} - -/** - * Error type for the `-32004` UnsupportedProtocolVersion protocol error (protocol - * revision 2026-07-28): the request's protocol version is unknown to the server or - * unsupported by it. - * - * The error data lists the protocol versions the receiver supports (`supported`), - * so the sender can choose a mutually supported version and retry, and echoes the - * version that was requested (`requested`). - */ -export class UnsupportedProtocolVersionError extends ProtocolError { - constructor(data: UnsupportedProtocolVersionErrorData, message: string = `Unsupported protocol version: ${data.requested}`) { - super(ProtocolErrorCode.UnsupportedProtocolVersion, message, data); - } - - /** - * Protocol versions the receiver supports. - */ - get supported(): string[] { - return (this.data as UnsupportedProtocolVersionErrorData).supported; - } - - /** - * The protocol version that was requested. - */ - get requested(): string { - return (this.data as UnsupportedProtocolVersionErrorData).requested; - } -} diff --git a/packages/core/src/types/guards.ts b/packages/core/src/types/guards.ts deleted file mode 100644 index 3bd069b0d8..0000000000 --- a/packages/core/src/types/guards.ts +++ /dev/null @@ -1,110 +0,0 @@ -import { - CallToolResultSchema, - InitializedNotificationSchema, - InitializeRequestSchema, - JSONRPCErrorResponseSchema, - JSONRPCMessageSchema, - JSONRPCNotificationSchema, - JSONRPCRequestSchema, - JSONRPCResponseSchema, - JSONRPCResultResponseSchema, - TaskAugmentedRequestParamsSchema -} from './schemas'; -import type { - CallToolResult, - CompleteRequest, - CompleteRequestPrompt, - CompleteRequestResourceTemplate, - InitializedNotification, - InitializeRequest, - JSONRPCErrorResponse, - JSONRPCMessage, - JSONRPCNotification, - JSONRPCRequest, - JSONRPCResponse, - JSONRPCResultResponse, - TaskAugmentedRequestParams -} from './types'; - -/** - * Validates and parses an unknown value as a JSON-RPC message. - * - * Use this to validate incoming messages in custom transport implementations. - * Throws if the value does not conform to the JSON-RPC message schema. - * - * @param value - The value to validate (typically a parsed JSON object). - * @returns The validated {@linkcode JSONRPCMessage}. - * @throws If validation fails. - */ -export function parseJSONRPCMessage(value: unknown): JSONRPCMessage { - return JSONRPCMessageSchema.parse(value); -} - -export const isJSONRPCRequest = (value: unknown): value is JSONRPCRequest => JSONRPCRequestSchema.safeParse(value).success; - -export const isJSONRPCNotification = (value: unknown): value is JSONRPCNotification => JSONRPCNotificationSchema.safeParse(value).success; - -/** - * Checks if a value is a valid {@linkcode JSONRPCResultResponse}. - * @param value - The value to check. - * - * @returns True if the value is a valid {@linkcode JSONRPCResultResponse}, false otherwise. - */ -export const isJSONRPCResultResponse = (value: unknown): value is JSONRPCResultResponse => - JSONRPCResultResponseSchema.safeParse(value).success; - -/** - * Checks if a value is a valid {@linkcode JSONRPCErrorResponse}. - * @param value - The value to check. - * - * @returns True if the value is a valid {@linkcode JSONRPCErrorResponse}, false otherwise. - */ -export const isJSONRPCErrorResponse = (value: unknown): value is JSONRPCErrorResponse => - JSONRPCErrorResponseSchema.safeParse(value).success; - -/** - * Checks if a value is a valid {@linkcode JSONRPCResponse} (either a result or error response). - * @param value - The value to check. - * - * @returns True if the value is a valid {@linkcode JSONRPCResponse}, false otherwise. - */ -export const isJSONRPCResponse = (value: unknown): value is JSONRPCResponse => JSONRPCResponseSchema.safeParse(value).success; - -/** - * Checks if a value is a valid {@linkcode CallToolResult}. - * @param value - The value to check. - * - * @returns True if the value is a valid {@linkcode CallToolResult}, false otherwise. - */ -export const isCallToolResult = (value: unknown): value is CallToolResult => { - if (typeof value !== 'object' || value === null || !('content' in value)) return false; - return CallToolResultSchema.safeParse(value).success; -}; - -/** - * Checks if a value is a valid {@linkcode TaskAugmentedRequestParams}. - * @param value - The value to check. - * - * @returns True if the value is a valid {@linkcode TaskAugmentedRequestParams}, false otherwise. - */ -export const isTaskAugmentedRequestParams = (value: unknown): value is TaskAugmentedRequestParams => - TaskAugmentedRequestParamsSchema.safeParse(value).success; - -export const isInitializeRequest = (value: unknown): value is InitializeRequest => InitializeRequestSchema.safeParse(value).success; - -export const isInitializedNotification = (value: unknown): value is InitializedNotification => - InitializedNotificationSchema.safeParse(value).success; - -export function assertCompleteRequestPrompt(request: CompleteRequest): asserts request is CompleteRequestPrompt { - if (request.params.ref.type !== 'ref/prompt') { - throw new TypeError(`Expected CompleteRequestPrompt, but got ${request.params.ref.type}`); - } - void (request as CompleteRequestPrompt); -} - -export function assertCompleteRequestResourceTemplate(request: CompleteRequest): asserts request is CompleteRequestResourceTemplate { - if (request.params.ref.type !== 'ref/resource') { - throw new TypeError(`Expected CompleteRequestResourceTemplate, but got ${request.params.ref.type}`); - } - void (request as CompleteRequestResourceTemplate); -} diff --git a/packages/core/src/types/index.ts b/packages/core/src/types/index.ts deleted file mode 100644 index bbb00b1e73..0000000000 --- a/packages/core/src/types/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -// Internal barrel — re-exports everything for use within the SDK packages. -// The public API is defined in @modelcontextprotocol/core/public (see exports/public/index.ts). -export * from './constants'; -export * from './enums'; -export * from './errors'; -export * from './guards'; -export * from './schemas'; -export * from './specTypeSchema'; -export * from './types'; diff --git a/packages/core/src/types/schemas.ts b/packages/core/src/types/schemas.ts deleted file mode 100644 index 3c942256fd..0000000000 --- a/packages/core/src/types/schemas.ts +++ /dev/null @@ -1,2346 +0,0 @@ -import * as z from 'zod/v4'; - -import { - CLIENT_CAPABILITIES_META_KEY, - CLIENT_INFO_META_KEY, - JSONRPC_VERSION, - LOG_LEVEL_META_KEY, - PROTOCOL_VERSION_META_KEY, - RELATED_TASK_META_KEY -} from './constants'; -import type { - JSONArray, - JSONObject, - JSONValue, - NotificationMethod, - NotificationTypeMap, - RequestMethod, - RequestTypeMap, - ResultTypeMap -} from './types'; - -export const JSONValueSchema: z.ZodType = z.lazy(() => - z.union([z.string(), z.number(), z.boolean(), z.null(), z.record(z.string(), JSONValueSchema), z.array(JSONValueSchema)]) -); -export const JSONObjectSchema: z.ZodType = z.record(z.string(), JSONValueSchema); -export const JSONArraySchema: z.ZodType = z.array(JSONValueSchema); -/** - * A progress token, used to associate progress notifications with the original request. - */ -export const ProgressTokenSchema = z.union([z.string(), z.number().int()]); - -/** - * An opaque token used to represent a cursor for pagination. - */ -export const CursorSchema = z.string(); - -/** - * Task creation parameters, used to ask that the server create a task to represent a request. - */ -export const TaskCreationParamsSchema = z.looseObject({ - /** - * Requested duration in milliseconds to retain task from creation. - */ - ttl: z.number().optional(), - - /** - * Time in milliseconds to wait between task status requests. - */ - pollInterval: z.number().optional() -}); - -export const TaskMetadataSchema = z.object({ - ttl: z.number().optional() -}); - -/** - * Metadata for associating messages with a task. - * Include this in the `_meta` field under the key `io.modelcontextprotocol/related-task`. - */ -export const RelatedTaskMetadataSchema = z.object({ - taskId: z.string() -}); - -export const RequestMetaSchema = z.looseObject({ - /** - * If specified, the caller is requesting out-of-band progress notifications for this request (as represented by notifications/progress). The value of this parameter is an opaque token that will be attached to any subsequent notifications. The receiver is not obligated to provide these notifications. - */ - progressToken: ProgressTokenSchema.optional(), - /** - * If specified, this request is related to the provided task. - */ - [RELATED_TASK_META_KEY]: RelatedTaskMetadataSchema.optional() -}); - -/** - * Common params for any request. - */ -export const BaseRequestParamsSchema = z.object({ - /** - * See [General fields: `_meta`](/specification/draft/basic/index#meta) for notes on `_meta` usage. - */ - _meta: RequestMetaSchema.optional() -}); - -/** - * Common params for any task-augmented request. - */ -export const TaskAugmentedRequestParamsSchema = BaseRequestParamsSchema.extend({ - /** - * If specified, the caller is requesting task-augmented execution for this request. - * The request will return a `CreateTaskResult` immediately, and the actual result can be - * retrieved later via `tasks/result`. - * - * Task augmentation is subject to capability negotiation - receivers MUST declare support - * for task augmentation of specific request types in their capabilities. - */ - task: TaskMetadataSchema.optional() -}); - -export const RequestSchema = z.object({ - method: z.string(), - params: BaseRequestParamsSchema.loose().optional() -}); - -export const NotificationsParamsSchema = z.object({ - /** - * See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields) - * for notes on `_meta` usage. - */ - _meta: RequestMetaSchema.optional() -}); - -export const NotificationSchema = z.object({ - method: z.string(), - params: NotificationsParamsSchema.loose().optional() -}); - -export const ResultSchema = z.looseObject({ - /** - * See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields) - * for notes on `_meta` usage. - */ - _meta: RequestMetaSchema.optional(), - /** - * Indicates the type of the result, allowing the receiver to determine how to - * parse the result object. Servers implementing protocol revision 2026-07-28 or - * later always include this field; results from earlier revisions omit it, and - * an absent value must be treated as `"complete"`. - */ - resultType: z.string().optional() -}); - -/** - * A uniquely identifying ID for a request in JSON-RPC. - */ -export const RequestIdSchema = z.union([z.string(), z.number().int()]); - -/** - * A request that expects a response. - */ -export const JSONRPCRequestSchema = z - .object({ - jsonrpc: z.literal(JSONRPC_VERSION), - id: RequestIdSchema, - ...RequestSchema.shape - }) - .strict(); - -/** - * A notification which does not expect a response. - */ -export const JSONRPCNotificationSchema = z - .object({ - jsonrpc: z.literal(JSONRPC_VERSION), - ...NotificationSchema.shape - }) - .strict(); - -/** - * A successful (non-error) response to a request. - */ -export const JSONRPCResultResponseSchema = z - .object({ - jsonrpc: z.literal(JSONRPC_VERSION), - id: RequestIdSchema, - result: ResultSchema - }) - .strict(); - -/** - * A response to a request that indicates an error occurred. - */ -export const JSONRPCErrorResponseSchema = z - .object({ - jsonrpc: z.literal(JSONRPC_VERSION), - id: RequestIdSchema.optional(), - error: z.object({ - /** - * The error type that occurred. - */ - code: z.number().int(), - /** - * A short description of the error. The message SHOULD be limited to a concise single sentence. - */ - message: z.string(), - /** - * Additional information about the error. The value of this member is defined by the sender (e.g. detailed error information, nested errors etc.). - */ - data: z.unknown().optional() - }) - }) - .strict(); - -export const JSONRPCMessageSchema = z.union([ - JSONRPCRequestSchema, - JSONRPCNotificationSchema, - JSONRPCResultResponseSchema, - JSONRPCErrorResponseSchema -]); - -export const JSONRPCResponseSchema = z.union([JSONRPCResultResponseSchema, JSONRPCErrorResponseSchema]); - -/* Empty result */ -/** - * A response that indicates success but carries no data. - */ -export const EmptyResultSchema = ResultSchema.strict(); - -export const CancelledNotificationParamsSchema = NotificationsParamsSchema.extend({ - /** - * The ID of the request to cancel. - * - * This MUST correspond to the ID of a request previously issued in the same direction. - */ - requestId: RequestIdSchema.optional(), - /** - * An optional string describing the reason for the cancellation. This MAY be logged or presented to the user. - */ - reason: z.string().optional() -}); -/* Cancellation */ -/** - * This notification can be sent by either side to indicate that it is cancelling a previously-issued request. - * - * The request SHOULD still be in-flight, but due to communication latency, it is always possible that this notification MAY arrive after the request has already finished. - * - * This notification indicates that the result will be unused, so any associated processing SHOULD cease. - * - * A client MUST NOT attempt to cancel its {@linkcode InitializeRequest | initialize} request. - */ -export const CancelledNotificationSchema = NotificationSchema.extend({ - method: z.literal('notifications/cancelled'), - params: CancelledNotificationParamsSchema -}); - -/* Base Metadata */ -/** - * Icon schema for use in {@link Tool | tools}, {@link Prompt | prompts}, {@link Resource | resources}, and {@link Implementation | implementations}. - */ -export const IconSchema = z.object({ - /** - * URL or data URI for the icon. - */ - src: z.string(), - /** - * Optional MIME type for the icon. - */ - mimeType: z.string().optional(), - /** - * Optional array of strings that specify sizes at which the icon can be used. - * Each string should be in WxH format (e.g., `"48x48"`, `"96x96"`) or `"any"` for scalable formats like SVG. - * - * If not provided, the client should assume that the icon can be used at any size. - */ - sizes: z.array(z.string()).optional(), - /** - * Optional specifier for the theme this icon is designed for. `light` indicates - * the icon is designed to be used with a light background, and `dark` indicates - * the icon is designed to be used with a dark background. - * - * If not provided, the client should assume the icon can be used with any theme. - */ - theme: z.enum(['light', 'dark']).optional() -}); - -/** - * Base schema to add `icons` property. - * - */ -export const IconsSchema = z.object({ - /** - * Optional set of sized icons that the client can display in a user interface. - * - * Clients that support rendering icons MUST support at least the following MIME types: - * - `image/png` - PNG images (safe, universal compatibility) - * - `image/jpeg` (and `image/jpg`) - JPEG images (safe, universal compatibility) - * - * Clients that support rendering icons SHOULD also support: - * - `image/svg+xml` - SVG images (scalable but requires security precautions) - * - `image/webp` - WebP images (modern, efficient format) - */ - icons: z.array(IconSchema).optional() -}); - -/** - * Base metadata interface for common properties across {@link Resource | resources}, {@link Tool | tools}, {@link Prompt | prompts}, and {@link Implementation | implementations}. - */ -export const BaseMetadataSchema = z.object({ - /** Intended for programmatic or logical use, but used as a display name in past specs or fallback */ - name: z.string(), - /** - * Intended for UI and end-user contexts — optimized to be human-readable and easily understood, - * even by those unfamiliar with domain-specific terminology. - * - * If not provided, the `name` should be used for display (except for `Tool`, - * where `annotations.title` should be given precedence over using `name`, - * if present). - */ - title: z.string().optional() -}); - -/* Initialization */ -/** - * Describes the name and version of an MCP implementation. - */ -export const ImplementationSchema = BaseMetadataSchema.extend({ - ...BaseMetadataSchema.shape, - ...IconsSchema.shape, - version: z.string(), - /** - * An optional URL of the website for this implementation. - */ - websiteUrl: z.string().optional(), - - /** - * An optional human-readable description of what this implementation does. - * - * This can be used by clients or servers to provide context about their purpose - * and capabilities. For example, a server might describe the types of resources - * or tools it provides, while a client might describe its intended use case. - */ - description: z.string().optional() -}); - -const FormElicitationCapabilitySchema = z.intersection( - z.object({ - applyDefaults: z.boolean().optional() - }), - JSONObjectSchema -); - -const ElicitationCapabilitySchema = z.preprocess( - value => { - if (value && typeof value === 'object' && !Array.isArray(value) && Object.keys(value as Record).length === 0) { - return { form: {} }; - } - return value; - }, - z.intersection( - z.object({ - form: FormElicitationCapabilitySchema.optional(), - url: JSONObjectSchema.optional() - }), - JSONObjectSchema.optional() - ) -); - -/** - * Task capabilities for clients, indicating which request types support task creation. - */ -export const ClientTasksCapabilitySchema = z.looseObject({ - /** - * Present if the client supports listing tasks. - */ - list: JSONObjectSchema.optional(), - /** - * Present if the client supports cancelling tasks. - */ - cancel: JSONObjectSchema.optional(), - /** - * Capabilities for task creation on specific request types. - */ - requests: z - .looseObject({ - /** - * Task support for sampling requests. - */ - sampling: z - .looseObject({ - createMessage: JSONObjectSchema.optional() - }) - .optional(), - /** - * Task support for elicitation requests. - */ - elicitation: z - .looseObject({ - create: JSONObjectSchema.optional() - }) - .optional() - }) - .optional() -}); - -/** - * Task capabilities for servers, indicating which request types support task creation. - */ -export const ServerTasksCapabilitySchema = z.looseObject({ - /** - * Present if the server supports listing tasks. - */ - list: JSONObjectSchema.optional(), - /** - * Present if the server supports cancelling tasks. - */ - cancel: JSONObjectSchema.optional(), - /** - * Capabilities for task creation on specific request types. - */ - requests: z - .looseObject({ - /** - * Task support for tool requests. - */ - tools: z - .looseObject({ - call: JSONObjectSchema.optional() - }) - .optional() - }) - .optional() -}); - -/** - * Capabilities a client may support. Known capabilities are defined here, in this schema, but this is not a closed set: any client can define its own, additional capabilities. - */ -export const ClientCapabilitiesSchema = z.object({ - /** - * Experimental, non-standard capabilities that the client supports. - */ - experimental: z.record(z.string(), JSONObjectSchema).optional(), - /** - * Present if the client supports sampling from an LLM. - * - * @deprecated Deprecated as of protocol version 2026-07-28 (SEP-2577); remains - * in the specification for at least twelve months. Migrate to calling LLM - * provider APIs directly. - */ - sampling: z - .object({ - /** - * Present if the client supports context inclusion via `includeContext` parameter. - * If not declared, servers SHOULD only use `includeContext: "none"` (or omit it). - */ - context: JSONObjectSchema.optional(), - /** - * Present if the client supports tool use via `tools` and `toolChoice` parameters. - */ - tools: JSONObjectSchema.optional() - }) - .optional(), - /** - * Present if the client supports eliciting user input. - */ - elicitation: ElicitationCapabilitySchema.optional(), - /** - * Present if the client supports listing roots. - * - * @deprecated Deprecated as of protocol version 2026-07-28 (SEP-2577); remains - * in the specification for at least twelve months. Migrate to passing paths via - * tool parameters, resource URIs, or configuration. - */ - roots: z - .object({ - /** - * Whether the client supports issuing notifications for changes to the roots list. - */ - listChanged: z.boolean().optional() - }) - .optional(), - /** - * Present if the client supports task creation. - */ - tasks: ClientTasksCapabilitySchema.optional(), - /** - * Extensions that the client supports. Keys are extension identifiers (vendor-prefix/extension-name). - */ - extensions: z.record(z.string(), JSONObjectSchema).optional() -}); - -export const InitializeRequestParamsSchema = BaseRequestParamsSchema.extend({ - /** - * The latest version of the Model Context Protocol that the client supports. The client MAY decide to support older versions as well. - */ - protocolVersion: z.string(), - capabilities: ClientCapabilitiesSchema, - clientInfo: ImplementationSchema -}); -/** - * This request is sent from the client to the server when it first connects, asking it to begin initialization. - */ -export const InitializeRequestSchema = RequestSchema.extend({ - method: z.literal('initialize'), - params: InitializeRequestParamsSchema -}); - -/** - * Capabilities that a server may support. Known capabilities are defined here, in this schema, but this is not a closed set: any server can define its own, additional capabilities. - */ -export const ServerCapabilitiesSchema = z.object({ - /** - * Experimental, non-standard capabilities that the server supports. - */ - experimental: z.record(z.string(), JSONObjectSchema).optional(), - /** - * Present if the server supports sending log messages to the client. - * - * @deprecated Deprecated as of protocol version 2026-07-28 (SEP-2577); remains - * in the specification for at least twelve months. Migrate to stderr logging - * (STDIO servers) or OpenTelemetry. - */ - logging: JSONObjectSchema.optional(), - /** - * Present if the server supports sending completions to the client. - */ - completions: JSONObjectSchema.optional(), - /** - * Present if the server offers any prompt templates. - */ - prompts: z - .object({ - /** - * Whether this server supports issuing notifications for changes to the prompt list. - */ - listChanged: z.boolean().optional() - }) - .optional(), - /** - * Present if the server offers any resources to read. - */ - resources: z - .object({ - /** - * Whether this server supports clients subscribing to resource updates. - */ - subscribe: z.boolean().optional(), - - /** - * Whether this server supports issuing notifications for changes to the resource list. - */ - listChanged: z.boolean().optional() - }) - .optional(), - /** - * Present if the server offers any tools to call. - */ - tools: z - .object({ - /** - * Whether this server supports issuing notifications for changes to the tool list. - */ - listChanged: z.boolean().optional() - }) - .optional(), - /** - * Present if the server supports task creation. - */ - tasks: ServerTasksCapabilitySchema.optional(), - /** - * Extensions that the server supports. Keys are extension identifiers (vendor-prefix/extension-name). - */ - extensions: z.record(z.string(), JSONObjectSchema).optional() -}); - -/** - * After receiving an initialize request from the client, the server sends this response. - */ -export const InitializeResultSchema = ResultSchema.extend({ - /** - * The version of the Model Context Protocol that the server wants to use. This may not match the version that the client requested. If the client cannot support this version, it MUST disconnect. - */ - protocolVersion: z.string(), - capabilities: ServerCapabilitiesSchema, - serverInfo: ImplementationSchema, - /** - * Instructions describing how to use the server and its features. - * - * This can be used by clients to improve the LLM's understanding of available tools, resources, etc. It can be thought of like a "hint" to the model. For example, this information MAY be added to the system prompt. - */ - instructions: z.string().optional() -}); - -/** - * This notification is sent from the client to the server after initialization has finished. - */ -export const InitializedNotificationSchema = NotificationSchema.extend({ - method: z.literal('notifications/initialized'), - params: NotificationsParamsSchema.optional() -}); - -/* Discovery */ -/** - * A request from the client asking the server to advertise its supported protocol - * versions, capabilities, and other metadata (protocol revision 2026-07-28). Servers - * MUST implement `server/discover`. Clients MAY call it but are not required to — - * version negotiation can also happen inline via the per-request `_meta` envelope. - */ -export const DiscoverRequestSchema = RequestSchema.extend({ - method: z.literal('server/discover'), - params: BaseRequestParamsSchema.optional() -}); - -/** - * The result returned by the server for a `server/discover` request. - */ -export const DiscoverResultSchema = ResultSchema.extend({ - /** - * MCP protocol versions this server supports. The client should choose a - * version from this list for use in subsequent requests. - */ - supportedVersions: z.array(z.string()), - /** - * The capabilities of the server. - */ - capabilities: ServerCapabilitiesSchema, - /** - * Information about the server software implementation. - */ - serverInfo: ImplementationSchema, - /** - * Instructions describing how to use the server and its features. - * - * This can be used by clients to improve the LLM's understanding of available tools, resources, etc. It can be thought of like a "hint" to the model. For example, this information MAY be added to the system prompt. - */ - instructions: z.string().optional() -}); - -/* Ping */ -/** - * A ping, issued by either the server or the client, to check that the other party is still alive. The receiver must promptly respond, or else may be disconnected. - */ -export const PingRequestSchema = RequestSchema.extend({ - method: z.literal('ping'), - params: BaseRequestParamsSchema.optional() -}); - -/* Progress notifications */ -export const ProgressSchema = z.object({ - /** - * The progress thus far. This should increase every time progress is made, even if the total is unknown. - */ - progress: z.number(), - /** - * Total number of items to process (or total progress required), if known. - */ - total: z.optional(z.number()), - /** - * An optional message describing the current progress. - */ - message: z.optional(z.string()) -}); - -export const ProgressNotificationParamsSchema = z.object({ - ...NotificationsParamsSchema.shape, - ...ProgressSchema.shape, - /** - * The progress token which was given in the initial request, used to associate this notification with the request that is proceeding. - */ - progressToken: ProgressTokenSchema -}); -/** - * An out-of-band notification used to inform the receiver of a progress update for a long-running request. - * - * @category notifications/progress - */ -export const ProgressNotificationSchema = NotificationSchema.extend({ - method: z.literal('notifications/progress'), - params: ProgressNotificationParamsSchema -}); - -export const PaginatedRequestParamsSchema = BaseRequestParamsSchema.extend({ - /** - * An opaque token representing the current pagination position. - * If provided, the server should return results starting after this cursor. - */ - cursor: CursorSchema.optional() -}); - -/* Pagination */ -export const PaginatedRequestSchema = RequestSchema.extend({ - params: PaginatedRequestParamsSchema.optional() -}); - -export const PaginatedResultSchema = ResultSchema.extend({ - /** - * An opaque token representing the pagination position after the last returned result. - * If present, there may be more results available. - */ - nextCursor: CursorSchema.optional() -}); - -/** - * The status of a task. - * */ -export const TaskStatusSchema = z.enum(['working', 'input_required', 'completed', 'failed', 'cancelled']); - -/* Tasks */ -/** - * A pollable state object associated with a request. - */ -export const TaskSchema = z.object({ - taskId: z.string(), - status: TaskStatusSchema, - /** - * Time in milliseconds to keep task results available after completion. - * If `null`, the task has unlimited lifetime until manually cleaned up. - */ - ttl: z.union([z.number(), z.null()]), - /** - * ISO 8601 timestamp when the task was created. - */ - createdAt: z.string(), - /** - * ISO 8601 timestamp when the task was last updated. - */ - lastUpdatedAt: z.string(), - pollInterval: z.optional(z.number()), - /** - * Optional diagnostic message for failed tasks or other status information. - */ - statusMessage: z.optional(z.string()) -}); - -/** - * Result returned when a task is created, containing the task data wrapped in a `task` field. - */ -export const CreateTaskResultSchema = ResultSchema.extend({ - task: TaskSchema -}); - -/** - * Parameters for task status notification. - */ -export const TaskStatusNotificationParamsSchema = NotificationsParamsSchema.merge(TaskSchema); - -/** - * A notification sent when a task's status changes. - */ -export const TaskStatusNotificationSchema = NotificationSchema.extend({ - method: z.literal('notifications/tasks/status'), - params: TaskStatusNotificationParamsSchema -}); - -/** - * A request to get the state of a specific task. - */ -export const GetTaskRequestSchema = RequestSchema.extend({ - method: z.literal('tasks/get'), - params: BaseRequestParamsSchema.extend({ - taskId: z.string() - }) -}); - -/** - * The response to a {@linkcode GetTaskRequest | tasks/get} request. - */ -export const GetTaskResultSchema = ResultSchema.merge(TaskSchema); - -/** - * A request to get the result of a specific task. - */ -export const GetTaskPayloadRequestSchema = RequestSchema.extend({ - method: z.literal('tasks/result'), - params: BaseRequestParamsSchema.extend({ - taskId: z.string() - }) -}); - -/** - * The response to a `tasks/result` request. - * The structure matches the result type of the original request. - * For example, a {@linkcode CallToolRequest | tools/call} task would return the `CallToolResult` structure. - * - */ -export const GetTaskPayloadResultSchema = ResultSchema.loose(); - -/** - * A request to list tasks. - */ -export const ListTasksRequestSchema = PaginatedRequestSchema.extend({ - method: z.literal('tasks/list') -}); - -/** - * The response to a {@linkcode ListTasksRequest | tasks/list} request. - */ -export const ListTasksResultSchema = PaginatedResultSchema.extend({ - tasks: z.array(TaskSchema) -}); - -/** - * A request to cancel a specific task. - */ -export const CancelTaskRequestSchema = RequestSchema.extend({ - method: z.literal('tasks/cancel'), - params: BaseRequestParamsSchema.extend({ - taskId: z.string() - }) -}); - -/** - * The response to a {@linkcode CancelTaskRequest | tasks/cancel} request. - */ -export const CancelTaskResultSchema = ResultSchema.merge(TaskSchema); - -/* Resources */ -/** - * The contents of a specific resource or sub-resource. - */ -export const ResourceContentsSchema = z.object({ - /** - * The URI of this resource. - */ - uri: z.string(), - /** - * The MIME type of this resource, if known. - */ - mimeType: z.optional(z.string()), - /** - * See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields) - * for notes on `_meta` usage. - */ - _meta: z.record(z.string(), z.unknown()).optional() -}); - -export const TextResourceContentsSchema = ResourceContentsSchema.extend({ - /** - * The text of the item. This must only be set if the item can actually be represented as text (not binary data). - */ - text: z.string() -}); - -/** - * A Zod schema for validating Base64 strings that is more performant and - * robust for very large inputs than the default regex-based check. It avoids - * stack overflows by using the native `atob` function for validation. - */ -const Base64Schema = z.string().refine( - val => { - try { - // atob throws a DOMException if the string contains characters - // that are not part of the Base64 character set. - atob(val); - return true; - } catch { - return false; - } - }, - { message: 'Invalid Base64 string' } -); - -export const BlobResourceContentsSchema = ResourceContentsSchema.extend({ - /** - * A base64-encoded string representing the binary data of the item. - */ - blob: Base64Schema -}); - -/** - * The sender or recipient of messages and data in a conversation. - */ -export const RoleSchema = z.enum(['user', 'assistant']); - -/** - * Optional annotations providing clients additional context about a resource. - */ -export const AnnotationsSchema = z.object({ - /** - * Intended audience(s) for the resource. - */ - audience: z.array(RoleSchema).optional(), - - /** - * Importance hint for the resource, from 0 (least) to 1 (most). - */ - priority: z.number().min(0).max(1).optional(), - - /** - * ISO 8601 timestamp for the most recent modification. - */ - lastModified: z.iso.datetime({ offset: true }).optional() -}); - -/** - * A known resource that the server is capable of reading. - */ -export const ResourceSchema = z.object({ - ...BaseMetadataSchema.shape, - ...IconsSchema.shape, - /** - * The URI of this resource. - */ - uri: z.string(), - - /** - * A description of what this resource represents. - * - * This can be used by clients to improve the LLM's understanding of available resources. It can be thought of like a "hint" to the model. - */ - description: z.optional(z.string()), - - /** - * The MIME type of this resource, if known. - */ - mimeType: z.optional(z.string()), - - /** - * The size of the raw resource content, in bytes (i.e., before base64 encoding or any tokenization), if known. - * - * This can be used by Hosts to display file sizes and estimate context window usage. - */ - size: z.optional(z.number()), - - /** - * Optional annotations for the client. - */ - annotations: AnnotationsSchema.optional(), - - /** - * See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields) - * for notes on `_meta` usage. - */ - _meta: z.optional(z.looseObject({})) -}); - -/** - * A template description for resources available on the server. - */ -export const ResourceTemplateSchema = z.object({ - ...BaseMetadataSchema.shape, - ...IconsSchema.shape, - /** - * A URI template (according to RFC 6570) that can be used to construct resource URIs. - */ - uriTemplate: z.string(), - - /** - * A description of what this template is for. - * - * This can be used by clients to improve the LLM's understanding of available resources. It can be thought of like a "hint" to the model. - */ - description: z.optional(z.string()), - - /** - * The MIME type for all resources that match this template. This should only be included if all resources matching this template have the same type. - */ - mimeType: z.optional(z.string()), - - /** - * Optional annotations for the client. - */ - annotations: AnnotationsSchema.optional(), - - /** - * See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields) - * for notes on `_meta` usage. - */ - _meta: z.optional(z.looseObject({})) -}); - -/** - * Sent from the client to request a list of resources the server has. - */ -export const ListResourcesRequestSchema = PaginatedRequestSchema.extend({ - method: z.literal('resources/list') -}); - -/** - * The server's response to a {@linkcode ListResourcesRequest | resources/list} request from the client. - */ -export const ListResourcesResultSchema = PaginatedResultSchema.extend({ - resources: z.array(ResourceSchema) -}); - -/** - * Sent from the client to request a list of resource templates the server has. - */ -export const ListResourceTemplatesRequestSchema = PaginatedRequestSchema.extend({ - method: z.literal('resources/templates/list') -}); - -/** - * The server's response to a {@linkcode ListResourceTemplatesRequest | resources/templates/list} request from the client. - */ -export const ListResourceTemplatesResultSchema = PaginatedResultSchema.extend({ - resourceTemplates: z.array(ResourceTemplateSchema) -}); - -export const ResourceRequestParamsSchema = BaseRequestParamsSchema.extend({ - /** - * The URI of the resource to read. The URI can use any protocol; it is up to the server how to interpret it. - * - * @format uri - */ - uri: z.string() -}); - -/** - * Parameters for a {@linkcode ReadResourceRequest | resources/read} request. - */ -export const ReadResourceRequestParamsSchema = ResourceRequestParamsSchema; - -/** - * Sent from the client to the server, to read a specific resource URI. - */ -export const ReadResourceRequestSchema = RequestSchema.extend({ - method: z.literal('resources/read'), - params: ReadResourceRequestParamsSchema -}); - -/** - * The server's response to a {@linkcode ReadResourceRequest | resources/read} request from the client. - */ -export const ReadResourceResultSchema = ResultSchema.extend({ - contents: z.array(z.union([TextResourceContentsSchema, BlobResourceContentsSchema])) -}); - -/** - * An optional notification from the server to the client, informing it that the list of resources it can read from has changed. This may be issued by servers without any previous subscription from the client. - */ -export const ResourceListChangedNotificationSchema = NotificationSchema.extend({ - method: z.literal('notifications/resources/list_changed'), - params: NotificationsParamsSchema.optional() -}); - -export const SubscribeRequestParamsSchema = ResourceRequestParamsSchema; -/** - * Sent from the client to request `resources/updated` notifications from the server whenever a particular resource changes. - */ -export const SubscribeRequestSchema = RequestSchema.extend({ - method: z.literal('resources/subscribe'), - params: SubscribeRequestParamsSchema -}); - -export const UnsubscribeRequestParamsSchema = ResourceRequestParamsSchema; -/** - * Sent from the client to request cancellation of {@linkcode ResourceUpdatedNotification | resources/updated} notifications from the server. This should follow a previous {@linkcode SubscribeRequest | resources/subscribe} request. - */ -export const UnsubscribeRequestSchema = RequestSchema.extend({ - method: z.literal('resources/unsubscribe'), - params: UnsubscribeRequestParamsSchema -}); - -/** - * Parameters for a {@linkcode ResourceUpdatedNotification | notifications/resources/updated} notification. - */ -export const ResourceUpdatedNotificationParamsSchema = NotificationsParamsSchema.extend({ - /** - * The URI of the resource that has been updated. This might be a sub-resource of the one that the client actually subscribed to. - */ - uri: z.string() -}); - -/** - * A notification from the server to the client, informing it that a resource has changed and may need to be read again. This should only be sent if the client previously sent a {@linkcode SubscribeRequest | resources/subscribe} request. - */ -export const ResourceUpdatedNotificationSchema = NotificationSchema.extend({ - method: z.literal('notifications/resources/updated'), - params: ResourceUpdatedNotificationParamsSchema -}); - -/* Prompts */ -/** - * Describes an argument that a prompt can accept. - */ -export const PromptArgumentSchema = z.object({ - /** - * The name of the argument. - */ - name: z.string(), - /** - * A human-readable description of the argument. - */ - description: z.optional(z.string()), - /** - * Whether this argument must be provided. - */ - required: z.optional(z.boolean()) -}); - -/** - * A prompt or prompt template that the server offers. - */ -export const PromptSchema = z.object({ - ...BaseMetadataSchema.shape, - ...IconsSchema.shape, - /** - * An optional description of what this prompt provides - */ - description: z.optional(z.string()), - /** - * A list of arguments to use for templating the prompt. - */ - arguments: z.optional(z.array(PromptArgumentSchema)), - /** - * See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields) - * for notes on `_meta` usage. - */ - _meta: z.optional(z.looseObject({})) -}); - -/** - * Sent from the client to request a list of prompts and prompt templates the server has. - */ -export const ListPromptsRequestSchema = PaginatedRequestSchema.extend({ - method: z.literal('prompts/list') -}); - -/** - * The server's response to a {@linkcode ListPromptsRequest | prompts/list} request from the client. - */ -export const ListPromptsResultSchema = PaginatedResultSchema.extend({ - prompts: z.array(PromptSchema) -}); - -/** - * Parameters for a {@linkcode GetPromptRequest | prompts/get} request. - */ -export const GetPromptRequestParamsSchema = BaseRequestParamsSchema.extend({ - /** - * The name of the prompt or prompt template. - */ - name: z.string(), - /** - * Arguments to use for templating the prompt. - */ - arguments: z.record(z.string(), z.string()).optional() -}); -/** - * Used by the client to get a prompt provided by the server. - */ -export const GetPromptRequestSchema = RequestSchema.extend({ - method: z.literal('prompts/get'), - params: GetPromptRequestParamsSchema -}); - -/** - * Text provided to or from an LLM. - */ -export const TextContentSchema = z.object({ - type: z.literal('text'), - /** - * The text content of the message. - */ - text: z.string(), - - /** - * Optional annotations for the client. - */ - annotations: AnnotationsSchema.optional(), - - /** - * See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields) - * for notes on `_meta` usage. - */ - _meta: z.record(z.string(), z.unknown()).optional() -}); - -/** - * An image provided to or from an LLM. - */ -export const ImageContentSchema = z.object({ - type: z.literal('image'), - /** - * The base64-encoded image data. - */ - data: Base64Schema, - /** - * The MIME type of the image. Different providers may support different image types. - */ - mimeType: z.string(), - - /** - * Optional annotations for the client. - */ - annotations: AnnotationsSchema.optional(), - - /** - * See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields) - * for notes on `_meta` usage. - */ - _meta: z.record(z.string(), z.unknown()).optional() -}); - -/** - * Audio content provided to or from an LLM. - */ -export const AudioContentSchema = z.object({ - type: z.literal('audio'), - /** - * The base64-encoded audio data. - */ - data: Base64Schema, - /** - * The MIME type of the audio. Different providers may support different audio types. - */ - mimeType: z.string(), - - /** - * Optional annotations for the client. - */ - annotations: AnnotationsSchema.optional(), - - /** - * See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields) - * for notes on `_meta` usage. - */ - _meta: z.record(z.string(), z.unknown()).optional() -}); - -/** - * A tool call request from an assistant (LLM). - * Represents the assistant's request to use a tool. - */ -export const ToolUseContentSchema = z.object({ - type: z.literal('tool_use'), - /** - * The name of the tool to invoke. - * Must match a tool name from the request's tools array. - */ - name: z.string(), - /** - * Unique identifier for this tool call. - * Used to correlate with `ToolResultContent` in subsequent messages. - */ - id: z.string(), - /** - * Arguments to pass to the tool. - * Must conform to the tool's `inputSchema`. - */ - input: z.record(z.string(), z.unknown()), - /** - * See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields) - * for notes on `_meta` usage. - */ - _meta: z.record(z.string(), z.unknown()).optional() -}); - -/** - * The contents of a resource, embedded into a prompt or tool call result. - */ -export const EmbeddedResourceSchema = z.object({ - type: z.literal('resource'), - resource: z.union([TextResourceContentsSchema, BlobResourceContentsSchema]), - /** - * Optional annotations for the client. - */ - annotations: AnnotationsSchema.optional(), - /** - * See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields) - * for notes on `_meta` usage. - */ - _meta: z.record(z.string(), z.unknown()).optional() -}); - -/** - * A resource that the server is capable of reading, included in a prompt or tool call result. - * - * Note: resource links returned by tools are not guaranteed to appear in the results of {@linkcode ListResourcesRequest | resources/list} requests. - */ -export const ResourceLinkSchema = ResourceSchema.extend({ - type: z.literal('resource_link') -}); - -/** - * A content block that can be used in prompts and tool results. - */ -export const ContentBlockSchema = z.union([ - TextContentSchema, - ImageContentSchema, - AudioContentSchema, - ResourceLinkSchema, - EmbeddedResourceSchema -]); - -/** - * Describes a message returned as part of a prompt. - */ -export const PromptMessageSchema = z.object({ - role: RoleSchema, - content: ContentBlockSchema -}); - -/** - * The server's response to a {@linkcode GetPromptRequest | prompts/get} request from the client. - */ -export const GetPromptResultSchema = ResultSchema.extend({ - /** - * An optional description for the prompt. - */ - description: z.string().optional(), - messages: z.array(PromptMessageSchema) -}); - -/** - * An optional notification from the server to the client, informing it that the list of prompts it offers has changed. This may be issued by servers without any previous subscription from the client. - */ -export const PromptListChangedNotificationSchema = NotificationSchema.extend({ - method: z.literal('notifications/prompts/list_changed'), - params: NotificationsParamsSchema.optional() -}); - -/* Tools */ -/** - * Additional properties describing a `Tool` to clients. - * - * NOTE: all properties in {@linkcode ToolAnnotations} are **hints**. - * They are not guaranteed to provide a faithful description of - * tool behavior (including descriptive properties like `title`). - * - * Clients should never make tool use decisions based on `ToolAnnotations` - * received from untrusted servers. - */ -export const ToolAnnotationsSchema = z.object({ - /** - * A human-readable title for the tool. - */ - title: z.string().optional(), - - /** - * If `true`, the tool does not modify its environment. - * - * Default: `false` - */ - readOnlyHint: z.boolean().optional(), - - /** - * If `true`, the tool may perform destructive updates to its environment. - * If `false`, the tool performs only additive updates. - * - * (This property is meaningful only when `readOnlyHint == false`) - * - * Default: `true` - */ - destructiveHint: z.boolean().optional(), - - /** - * If `true`, calling the tool repeatedly with the same arguments - * will have no additional effect on its environment. - * - * (This property is meaningful only when `readOnlyHint == false`) - * - * Default: `false` - */ - idempotentHint: z.boolean().optional(), - - /** - * If `true`, this tool may interact with an "open world" of external - * entities. If `false`, the tool's domain of interaction is closed. - * For example, the world of a web search tool is open, whereas that - * of a memory tool is not. - * - * Default: `true` - */ - openWorldHint: z.boolean().optional() -}); - -/** - * Execution-related properties for a tool. - */ -export const ToolExecutionSchema = z.object({ - /** - * Indicates the tool's preference for task-augmented execution. - * - `"required"`: Clients MUST invoke the tool as a task - * - `"optional"`: Clients MAY invoke the tool as a task or normal request - * - `"forbidden"`: Clients MUST NOT attempt to invoke the tool as a task - * - * If not present, defaults to `"forbidden"`. - */ - taskSupport: z.enum(['required', 'optional', 'forbidden']).optional() -}); - -/** - * Definition for a tool the client can call. - */ -export const ToolSchema = z.object({ - ...BaseMetadataSchema.shape, - ...IconsSchema.shape, - /** - * A human-readable description of the tool. - */ - description: z.string().optional(), - /** - * A JSON Schema 2020-12 object defining the expected parameters for the tool. - * Must have `type: 'object'` at the root level per MCP spec. - */ - inputSchema: z - .object({ - type: z.literal('object'), - properties: z.record(z.string(), JSONValueSchema).optional(), - required: z.array(z.string()).optional() - }) - .catchall(z.unknown()), - /** - * An optional JSON Schema 2020-12 object defining the structure of the tool's output - * returned in the `structuredContent` field of a `CallToolResult`. - * Must have `type: 'object'` at the root level per MCP spec. - */ - outputSchema: z - .object({ - type: z.literal('object'), - properties: z.record(z.string(), JSONValueSchema).optional(), - required: z.array(z.string()).optional() - }) - .catchall(z.unknown()) - .optional(), - /** - * Optional additional tool information. - */ - annotations: ToolAnnotationsSchema.optional(), - /** - * Execution-related properties for this tool. - */ - execution: ToolExecutionSchema.optional(), - - /** - * See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields) - * for notes on `_meta` usage. - */ - _meta: z.record(z.string(), z.unknown()).optional() -}); - -/** - * Sent from the client to request a list of tools the server has. - */ -export const ListToolsRequestSchema = PaginatedRequestSchema.extend({ - method: z.literal('tools/list') -}); - -/** - * The server's response to a {@linkcode ListToolsRequest | tools/list} request from the client. - */ -export const ListToolsResultSchema = PaginatedResultSchema.extend({ - tools: z.array(ToolSchema) -}); - -/** - * The server's response to a tool call. - */ -export const CallToolResultSchema = ResultSchema.extend({ - /** - * A list of content objects that represent the result of the tool call. - * - * If the `Tool` does not define an outputSchema, this field MUST be present in the result. - * For backwards compatibility, this field is always present, but it may be empty. - */ - content: z.array(ContentBlockSchema).default([]), - - /** - * An object containing structured tool output. - * - * If the `Tool` defines an outputSchema, this field MUST be present in the result, and contain a JSON object that matches the schema. - */ - structuredContent: z.record(z.string(), z.unknown()).optional(), - - /** - * Whether the tool call ended in an error. - * - * If not set, this is assumed to be `false` (the call was successful). - * - * Any errors that originate from the tool SHOULD be reported inside the result - * object, with `isError` set to `true`, _not_ as an MCP protocol-level error - * response. Otherwise, the LLM would not be able to see that an error occurred - * and self-correct. - * - * However, any errors in _finding_ the tool, an error indicating that the - * server does not support tool calls, or any other exceptional conditions, - * should be reported as an MCP error response. - */ - isError: z.boolean().optional() -}); - -/** - * {@linkcode CallToolResultSchema} extended with backwards compatibility to protocol version 2024-10-07. - */ -export const CompatibilityCallToolResultSchema = CallToolResultSchema.or( - ResultSchema.extend({ - toolResult: z.unknown() - }) -); - -/** - * Parameters for a `tools/call` request. - */ -export const CallToolRequestParamsSchema = TaskAugmentedRequestParamsSchema.extend({ - /** - * The name of the tool to call. - */ - name: z.string(), - /** - * Arguments to pass to the tool. - */ - arguments: z.record(z.string(), z.unknown()).optional() -}); - -/** - * Used by the client to invoke a tool provided by the server. - */ -export const CallToolRequestSchema = RequestSchema.extend({ - method: z.literal('tools/call'), - params: CallToolRequestParamsSchema -}); - -/** - * An optional notification from the server to the client, informing it that the list of tools it offers has changed. This may be issued by servers without any previous subscription from the client. - */ -export const ToolListChangedNotificationSchema = NotificationSchema.extend({ - method: z.literal('notifications/tools/list_changed'), - params: NotificationsParamsSchema.optional() -}); - -/** - * Base schema for list changed subscription options (without callback). - * Used internally for Zod validation of `autoRefresh` and `debounceMs`. - */ -export const ListChangedOptionsBaseSchema = z.object({ - /** - * If `true`, the list will be refreshed automatically when a list changed notification is received. - * The callback will be called with the updated list. - * - * If `false`, the callback will be called with `null` items, allowing manual refresh. - * - * @default true - */ - autoRefresh: z.boolean().default(true), - /** - * Debounce time in milliseconds for list changed notification processing. - * - * Multiple notifications received within this timeframe will only trigger one refresh. - * Set to `0` to disable debouncing. - * - * @default 300 - */ - debounceMs: z.number().int().nonnegative().default(300) -}); - -/* Logging */ -/** - * The severity of a log message. - */ -export const LoggingLevelSchema = z.enum(['debug', 'info', 'notice', 'warning', 'error', 'critical', 'alert', 'emergency']); - -/** - * Parameters for a `logging/setLevel` request. - */ -export const SetLevelRequestParamsSchema = BaseRequestParamsSchema.extend({ - /** - * The level of logging that the client wants to receive from the server. The server should send all logs at this level and higher (i.e., more severe) to the client as `notifications/logging/message`. - */ - level: LoggingLevelSchema -}); -/** - * A request from the client to the server, to enable or adjust logging. - */ -export const SetLevelRequestSchema = RequestSchema.extend({ - method: z.literal('logging/setLevel'), - params: SetLevelRequestParamsSchema -}); - -/** - * Parameters for a `notifications/message` notification. - */ -export const LoggingMessageNotificationParamsSchema = NotificationsParamsSchema.extend({ - /** - * The severity of this log message. - */ - level: LoggingLevelSchema, - /** - * An optional name of the logger issuing this message. - */ - logger: z.string().optional(), - /** - * The data to be logged, such as a string message or an object. Any JSON serializable type is allowed here. - */ - data: z.unknown() -}); -/** - * Notification of a log message passed from server to client. If no `logging/setLevel` request has been sent from the client, the server MAY decide which messages to send automatically. - */ -export const LoggingMessageNotificationSchema = NotificationSchema.extend({ - method: z.literal('notifications/message'), - params: LoggingMessageNotificationParamsSchema -}); - -/* Per-request `_meta` envelope */ -/** - * The per-request `_meta` envelope carried by every request under protocol revision - * 2026-07-28: the protocol version governing the request, the client implementation - * info, and the client's capabilities — declared per request rather than once at - * initialization — plus the optional log-level opt-in. - * - * This schema models the complete envelope on its own. The base request schemas - * ({@linkcode RequestMetaSchema}) deliberately stay lenient so the same wire schemas - * parse requests from earlier protocol revisions (no envelope) as well; envelope - * requiredness is enforced per request at dispatch time, not here. - */ -export const RequestMetaEnvelopeSchema = z.looseObject({ - /** - * If specified, the caller is requesting out-of-band progress notifications for this request (as represented by notifications/progress). The value of this parameter is an opaque token that will be attached to any subsequent notifications. The receiver is not obligated to provide these notifications. - */ - progressToken: ProgressTokenSchema.optional(), - /** - * The MCP protocol version being used for this request. For the HTTP transport, - * the value must match the `MCP-Protocol-Version` header. - */ - [PROTOCOL_VERSION_META_KEY]: z.string(), - /** - * Identifies the client software making the request. - */ - [CLIENT_INFO_META_KEY]: ImplementationSchema, - /** - * The client's capabilities for this specific request. An empty object means the - * client supports no optional capabilities. Servers must not infer capabilities - * from prior requests. - */ - [CLIENT_CAPABILITIES_META_KEY]: ClientCapabilitiesSchema, - /** - * The desired log level for this request. When absent, the server must not send - * `notifications/message` notifications for the request. - * - * @deprecated Deprecated as of protocol version 2026-07-28 (SEP-2577); remains - * in the specification for at least twelve months. - */ - [LOG_LEVEL_META_KEY]: LoggingLevelSchema.optional() -}); - -/* Sampling */ -/** - * Hints to use for model selection. - */ -export const ModelHintSchema = z.object({ - /** - * A hint for a model name. - */ - name: z.string().optional() -}); - -/** - * The server's preferences for model selection, requested of the client during sampling. - */ -export const ModelPreferencesSchema = z.object({ - /** - * Optional hints to use for model selection. - */ - hints: z.array(ModelHintSchema).optional(), - /** - * How much to prioritize cost when selecting a model. - */ - costPriority: z.number().min(0).max(1).optional(), - /** - * How much to prioritize sampling speed (latency) when selecting a model. - */ - speedPriority: z.number().min(0).max(1).optional(), - /** - * How much to prioritize intelligence and capabilities when selecting a model. - */ - intelligencePriority: z.number().min(0).max(1).optional() -}); - -/** - * Controls tool usage behavior in sampling requests. - */ -export const ToolChoiceSchema = z.object({ - /** - * Controls when tools are used: - * - `"auto"`: Model decides whether to use tools (default) - * - `"required"`: Model MUST use at least one tool before completing - * - `"none"`: Model MUST NOT use any tools - */ - mode: z.enum(['auto', 'required', 'none']).optional() -}); - -/** - * The result of a tool execution, provided by the user (server). - * Represents the outcome of invoking a tool requested via `ToolUseContent`. - */ -export const ToolResultContentSchema = z.object({ - type: z.literal('tool_result'), - toolUseId: z.string().describe('The unique identifier for the corresponding tool call.'), - content: z.array(ContentBlockSchema).default([]), - structuredContent: z.object({}).loose().optional(), - isError: z.boolean().optional(), - - /** - * See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields) - * for notes on `_meta` usage. - */ - _meta: z.record(z.string(), z.unknown()).optional() -}); - -/** - * Basic content types for sampling responses (without tool use). - * Used for backwards-compatible {@linkcode CreateMessageResult} when tools are not used. - */ -export const SamplingContentSchema = z.discriminatedUnion('type', [TextContentSchema, ImageContentSchema, AudioContentSchema]); - -/** - * Content block types allowed in sampling messages. - * This includes text, image, audio, tool use requests, and tool results. - */ -export const SamplingMessageContentBlockSchema = z.discriminatedUnion('type', [ - TextContentSchema, - ImageContentSchema, - AudioContentSchema, - ToolUseContentSchema, - ToolResultContentSchema -]); - -/** - * Describes a message issued to or received from an LLM API. - */ -export const SamplingMessageSchema = z.object({ - role: RoleSchema, - content: z.union([SamplingMessageContentBlockSchema, z.array(SamplingMessageContentBlockSchema)]), - /** - * See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields) - * for notes on `_meta` usage. - */ - _meta: z.record(z.string(), z.unknown()).optional() -}); - -/** - * Parameters for a `sampling/createMessage` request. - */ -export const CreateMessageRequestParamsSchema = TaskAugmentedRequestParamsSchema.extend({ - messages: z.array(SamplingMessageSchema), - /** - * The server's preferences for which model to select. The client MAY modify or omit this request. - */ - modelPreferences: ModelPreferencesSchema.optional(), - /** - * An optional system prompt the server wants to use for sampling. The client MAY modify or omit this prompt. - */ - systemPrompt: z.string().optional(), - /** - * A request to include context from one or more MCP servers (including the caller), to be attached to the prompt. - * The client MAY ignore this request. - * - * Default is `"none"`. Values `"thisServer"` and `"allServers"` are soft-deprecated. Servers SHOULD only use these values if the client - * declares `ClientCapabilities`.`sampling.context`. These values may be removed in future spec releases. - */ - includeContext: z.enum(['none', 'thisServer', 'allServers']).optional(), - temperature: z.number().optional(), - /** - * The requested maximum number of tokens to sample (to prevent runaway completions). - * - * The client MAY choose to sample fewer tokens than the requested maximum. - */ - maxTokens: z.number().int(), - stopSequences: z.array(z.string()).optional(), - /** - * Optional metadata to pass through to the LLM provider. The format of this metadata is provider-specific. - */ - metadata: JSONObjectSchema.optional(), - /** - * Tools that the model may use during generation. - * The client MUST return an error if this field is provided but `ClientCapabilities`.`sampling.tools` is not declared. - */ - tools: z.array(ToolSchema).optional(), - /** - * Controls how the model uses tools. - * The client MUST return an error if this field is provided but `ClientCapabilities`.`sampling.tools` is not declared. - * Default is `{ mode: "auto" }`. - */ - toolChoice: ToolChoiceSchema.optional() -}); -/** - * A request from the server to sample an LLM via the client. The client has full discretion over which model to select. The client should also inform the user before beginning sampling, to allow them to inspect the request (human in the loop) and decide whether to approve it. - */ -export const CreateMessageRequestSchema = RequestSchema.extend({ - method: z.literal('sampling/createMessage'), - params: CreateMessageRequestParamsSchema -}); - -/** - * The client's response to a `sampling/create_message` request from the server. - * This is the backwards-compatible version that returns single content (no arrays). - * Used when the request does not include tools. - */ -export const CreateMessageResultSchema = ResultSchema.extend({ - /** - * The name of the model that generated the message. - */ - model: z.string(), - /** - * The reason why sampling stopped, if known. - * - * Standard values: - * - `"endTurn"`: Natural end of the assistant's turn - * - `"stopSequence"`: A stop sequence was encountered - * - `"maxTokens"`: Maximum token limit was reached - * - * This field is an open string to allow for provider-specific stop reasons. - */ - stopReason: z.optional(z.enum(['endTurn', 'stopSequence', 'maxTokens']).or(z.string())), - role: RoleSchema, - /** - * Response content. Single content block (text, image, or audio). - */ - content: SamplingContentSchema -}); - -/** - * The client's response to a `sampling/create_message` request when tools were provided. - * This version supports array content for tool use flows. - */ -export const CreateMessageResultWithToolsSchema = ResultSchema.extend({ - /** - * The name of the model that generated the message. - */ - model: z.string(), - /** - * The reason why sampling stopped, if known. - * - * Standard values: - * - `"endTurn"`: Natural end of the assistant's turn - * - `"stopSequence"`: A stop sequence was encountered - * - `"maxTokens"`: Maximum token limit was reached - * - `"toolUse"`: The model wants to use one or more tools - * - * This field is an open string to allow for provider-specific stop reasons. - */ - stopReason: z.optional(z.enum(['endTurn', 'stopSequence', 'maxTokens', 'toolUse']).or(z.string())), - role: RoleSchema, - /** - * Response content. May be a single block or array. May include `ToolUseContent` if `stopReason` is `"toolUse"`. - */ - content: z.union([SamplingMessageContentBlockSchema, z.array(SamplingMessageContentBlockSchema)]) -}); - -/* Elicitation */ -/** - * Primitive schema definition for boolean fields. - */ -export const BooleanSchemaSchema = z.object({ - type: z.literal('boolean'), - title: z.string().optional(), - description: z.string().optional(), - default: z.boolean().optional() -}); - -/** - * Primitive schema definition for string fields. - */ -export const StringSchemaSchema = z.object({ - type: z.literal('string'), - title: z.string().optional(), - description: z.string().optional(), - minLength: z.number().optional(), - maxLength: z.number().optional(), - format: z.enum(['email', 'uri', 'date', 'date-time']).optional(), - default: z.string().optional() -}); - -/** - * Primitive schema definition for number fields. - */ -export const NumberSchemaSchema = z.object({ - type: z.enum(['number', 'integer']), - title: z.string().optional(), - description: z.string().optional(), - minimum: z.number().optional(), - maximum: z.number().optional(), - default: z.number().optional() -}); - -/** - * Schema for single-selection enumeration without display titles for options. - */ -export const UntitledSingleSelectEnumSchemaSchema = z.object({ - type: z.literal('string'), - title: z.string().optional(), - description: z.string().optional(), - enum: z.array(z.string()), - default: z.string().optional() -}); - -/** - * Schema for single-selection enumeration with display titles for each option. - */ -export const TitledSingleSelectEnumSchemaSchema = z.object({ - type: z.literal('string'), - title: z.string().optional(), - description: z.string().optional(), - oneOf: z.array( - z.object({ - const: z.string(), - title: z.string() - }) - ), - default: z.string().optional() -}); - -/** - * Use {@linkcode TitledSingleSelectEnumSchema} instead. - * This interface will be removed in a future version. - */ -export const LegacyTitledEnumSchemaSchema = z.object({ - type: z.literal('string'), - title: z.string().optional(), - description: z.string().optional(), - enum: z.array(z.string()), - enumNames: z.array(z.string()).optional(), - default: z.string().optional() -}); - -// Combined single selection enumeration -export const SingleSelectEnumSchemaSchema = z.union([UntitledSingleSelectEnumSchemaSchema, TitledSingleSelectEnumSchemaSchema]); - -/** - * Schema for multiple-selection enumeration without display titles for options. - */ -export const UntitledMultiSelectEnumSchemaSchema = z.object({ - type: z.literal('array'), - title: z.string().optional(), - description: z.string().optional(), - minItems: z.number().optional(), - maxItems: z.number().optional(), - items: z.object({ - type: z.literal('string'), - enum: z.array(z.string()) - }), - default: z.array(z.string()).optional() -}); - -/** - * Schema for multiple-selection enumeration with display titles for each option. - */ -export const TitledMultiSelectEnumSchemaSchema = z.object({ - type: z.literal('array'), - title: z.string().optional(), - description: z.string().optional(), - minItems: z.number().optional(), - maxItems: z.number().optional(), - items: z.object({ - anyOf: z.array( - z.object({ - const: z.string(), - title: z.string() - }) - ) - }), - default: z.array(z.string()).optional() -}); - -/** - * Combined schema for multiple-selection enumeration - */ -export const MultiSelectEnumSchemaSchema = z.union([UntitledMultiSelectEnumSchemaSchema, TitledMultiSelectEnumSchemaSchema]); - -/** - * Primitive schema definition for enum fields. - */ -export const EnumSchemaSchema = z.union([LegacyTitledEnumSchemaSchema, SingleSelectEnumSchemaSchema, MultiSelectEnumSchemaSchema]); - -/** - * Union of all primitive schema definitions. - */ -export const PrimitiveSchemaDefinitionSchema = z.union([EnumSchemaSchema, BooleanSchemaSchema, StringSchemaSchema, NumberSchemaSchema]); - -/** - * Parameters for an `elicitation/create` request for form-based elicitation. - */ -export const ElicitRequestFormParamsSchema = TaskAugmentedRequestParamsSchema.extend({ - /** - * The elicitation mode. - * - * Optional for backward compatibility. Clients MUST treat missing `mode` as `"form"`. - */ - mode: z.literal('form').optional(), - /** - * The message to present to the user describing what information is being requested. - */ - message: z.string(), - /** - * A restricted subset of JSON Schema. - * Only top-level properties are allowed, without nesting. - */ - requestedSchema: z - .object({ - type: z.literal('object'), - properties: z.record(z.string(), PrimitiveSchemaDefinitionSchema), - required: z.array(z.string()).optional() - }) - .catchall(z.unknown()) -}); - -/** - * Parameters for an {@linkcode ElicitRequest | elicitation/create} request for URL-based elicitation. - */ -export const ElicitRequestURLParamsSchema = TaskAugmentedRequestParamsSchema.extend({ - /** - * The elicitation mode. - */ - mode: z.literal('url'), - /** - * The message to present to the user explaining why the interaction is needed. - */ - message: z.string(), - /** - * The ID of the elicitation, which must be unique within the context of the server. - * The client MUST treat this ID as an opaque value. - */ - elicitationId: z.string(), - /** - * The URL that the user should navigate to. - */ - url: z.string().url() -}); - -/** - * The parameters for a request to elicit additional information from the user via the client. - */ -export const ElicitRequestParamsSchema = z.union([ElicitRequestFormParamsSchema, ElicitRequestURLParamsSchema]); - -/** - * A request from the server to elicit user input via the client. - * The client should present the message and form fields to the user (form mode) - * or navigate to a URL (URL mode). - */ -export const ElicitRequestSchema = RequestSchema.extend({ - method: z.literal('elicitation/create'), - params: ElicitRequestParamsSchema -}); - -/** - * Parameters for a {@linkcode ElicitationCompleteNotification | notifications/elicitation/complete} notification. - * - * @category notifications/elicitation/complete - */ -export const ElicitationCompleteNotificationParamsSchema = NotificationsParamsSchema.extend({ - /** - * The ID of the elicitation that completed. - */ - elicitationId: z.string() -}); - -/** - * A notification from the server to the client, informing it of a completion of an out-of-band elicitation request. - * - * @category notifications/elicitation/complete - */ -export const ElicitationCompleteNotificationSchema = NotificationSchema.extend({ - method: z.literal('notifications/elicitation/complete'), - params: ElicitationCompleteNotificationParamsSchema -}); - -/** - * The client's response to an {@linkcode ElicitRequest | elicitation/create} request from the server. - */ -export const ElicitResultSchema = ResultSchema.extend({ - /** - * The user action in response to the elicitation. - * - `"accept"`: User submitted the form/confirmed the action - * - `"decline"`: User explicitly declined the action - * - `"cancel"`: User dismissed without making an explicit choice - */ - action: z.enum(['accept', 'decline', 'cancel']), - /** - * The submitted form data, only present when action is `"accept"`. - * Contains values matching the requested schema. - * Per MCP spec, content is "typically omitted" for decline/cancel actions. - * We normalize `null` to `undefined` for leniency while maintaining type compatibility. - */ - content: z.preprocess( - val => (val === null ? undefined : val), - z.record(z.string(), z.union([z.string(), z.number(), z.boolean(), z.array(z.string())])).optional() - ) -}); - -/* Autocomplete */ -/** - * A reference to a resource or resource template definition. - */ -export const ResourceTemplateReferenceSchema = z.object({ - type: z.literal('ref/resource'), - /** - * The URI or URI template of the resource. - */ - uri: z.string() -}); - -/** - * Identifies a prompt. - */ -export const PromptReferenceSchema = z.object({ - type: z.literal('ref/prompt'), - /** - * The name of the prompt or prompt template - */ - name: z.string() -}); - -/** - * Parameters for a {@linkcode CompleteRequest | completion/complete} request. - */ -export const CompleteRequestParamsSchema = BaseRequestParamsSchema.extend({ - ref: z.union([PromptReferenceSchema, ResourceTemplateReferenceSchema]), - /** - * The argument's information - */ - argument: z.object({ - /** - * The name of the argument - */ - name: z.string(), - /** - * The value of the argument to use for completion matching. - */ - value: z.string() - }), - context: z - .object({ - /** - * Previously-resolved variables in a URI template or prompt. - */ - arguments: z.record(z.string(), z.string()).optional() - }) - .optional() -}); -/** - * A request from the client to the server, to ask for completion options. - */ -export const CompleteRequestSchema = RequestSchema.extend({ - method: z.literal('completion/complete'), - params: CompleteRequestParamsSchema -}); - -/** - * The server's response to a {@linkcode CompleteRequest | completion/complete} request - */ -export const CompleteResultSchema = ResultSchema.extend({ - completion: z.looseObject({ - /** - * An array of completion values. Must not exceed 100 items. - */ - values: z.array(z.string()).max(100), - /** - * The total number of completion options available. This can exceed the number of values actually sent in the response. - */ - total: z.optional(z.number().int()), - /** - * Indicates whether there are additional completion options beyond those provided in the current response, even if the exact total is unknown. - */ - hasMore: z.optional(z.boolean()) - }) -}); - -/* Roots */ -/** - * Represents a root directory or file that the server can operate on. - */ -export const RootSchema = z.object({ - /** - * The URI identifying the root. This *must* start with `file://` for now. - */ - uri: z.string().startsWith('file://'), - /** - * An optional name for the root. - */ - name: z.string().optional(), - - /** - * See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields) - * for notes on `_meta` usage. - */ - _meta: z.record(z.string(), z.unknown()).optional() -}); - -/** - * Sent from the server to request a list of root URIs from the client. - */ -export const ListRootsRequestSchema = RequestSchema.extend({ - method: z.literal('roots/list'), - params: BaseRequestParamsSchema.optional() -}); - -/** - * The client's response to a `roots/list` request from the server. - */ -export const ListRootsResultSchema = ResultSchema.extend({ - roots: z.array(RootSchema) -}); - -/** - * A notification from the client to the server, informing it that the list of roots has changed. - */ -export const RootsListChangedNotificationSchema = NotificationSchema.extend({ - method: z.literal('notifications/roots/list_changed'), - params: NotificationsParamsSchema.optional() -}); - -/* Client messages */ -export const ClientRequestSchema = z.union([ - PingRequestSchema, - InitializeRequestSchema, - CompleteRequestSchema, - SetLevelRequestSchema, - GetPromptRequestSchema, - ListPromptsRequestSchema, - ListResourcesRequestSchema, - ListResourceTemplatesRequestSchema, - ReadResourceRequestSchema, - SubscribeRequestSchema, - UnsubscribeRequestSchema, - CallToolRequestSchema, - ListToolsRequestSchema, - GetTaskRequestSchema, - GetTaskPayloadRequestSchema, - ListTasksRequestSchema, - CancelTaskRequestSchema -]); - -export const ClientNotificationSchema = z.union([ - CancelledNotificationSchema, - ProgressNotificationSchema, - InitializedNotificationSchema, - RootsListChangedNotificationSchema, - TaskStatusNotificationSchema -]); - -export const ClientResultSchema = z.union([ - EmptyResultSchema, - CreateMessageResultSchema, - CreateMessageResultWithToolsSchema, - ElicitResultSchema, - ListRootsResultSchema, - GetTaskResultSchema, - ListTasksResultSchema, - CreateTaskResultSchema -]); - -/* Server messages */ -export const ServerRequestSchema = z.union([ - PingRequestSchema, - CreateMessageRequestSchema, - ElicitRequestSchema, - ListRootsRequestSchema, - GetTaskRequestSchema, - GetTaskPayloadRequestSchema, - ListTasksRequestSchema, - CancelTaskRequestSchema -]); - -export const ServerNotificationSchema = z.union([ - CancelledNotificationSchema, - ProgressNotificationSchema, - LoggingMessageNotificationSchema, - ResourceUpdatedNotificationSchema, - ResourceListChangedNotificationSchema, - ToolListChangedNotificationSchema, - PromptListChangedNotificationSchema, - TaskStatusNotificationSchema, - ElicitationCompleteNotificationSchema -]); - -export const ServerResultSchema = z.union([ - EmptyResultSchema, - InitializeResultSchema, - CompleteResultSchema, - GetPromptResultSchema, - ListPromptsResultSchema, - ListResourcesResultSchema, - ListResourceTemplatesResultSchema, - ReadResourceResultSchema, - CallToolResultSchema, - ListToolsResultSchema, - GetTaskResultSchema, - ListTasksResultSchema, - CreateTaskResultSchema -]); - -/* Runtime schema lookup — result schemas by method */ -const resultSchemas: Record = { - ping: EmptyResultSchema, - initialize: InitializeResultSchema, - 'completion/complete': CompleteResultSchema, - 'logging/setLevel': EmptyResultSchema, - 'prompts/get': GetPromptResultSchema, - 'prompts/list': ListPromptsResultSchema, - 'resources/list': ListResourcesResultSchema, - 'resources/templates/list': ListResourceTemplatesResultSchema, - 'resources/read': ReadResourceResultSchema, - 'resources/subscribe': EmptyResultSchema, - 'resources/unsubscribe': EmptyResultSchema, - 'tools/call': z.union([CallToolResultSchema, CreateTaskResultSchema]), - 'tools/list': ListToolsResultSchema, - 'sampling/createMessage': z.union([CreateMessageResultWithToolsSchema, CreateTaskResultSchema]), - 'elicitation/create': z.union([ElicitResultSchema, CreateTaskResultSchema]), - 'roots/list': ListRootsResultSchema, - 'tasks/get': GetTaskResultSchema, - 'tasks/result': ResultSchema, - 'tasks/list': ListTasksResultSchema, - 'tasks/cancel': CancelTaskResultSchema -}; - -/** - * Gets the Zod schema for validating results of a given request method. - * Returns `undefined` for non-spec methods. - * @see getRequestSchema for explanation of the internal type assertion. - */ -export function getResultSchema(method: M): z.ZodType; -export function getResultSchema(method: string): z.ZodType | undefined; -export function getResultSchema(method: string): z.ZodType | undefined { - return resultSchemas[method as RequestMethod] as unknown as z.ZodType | undefined; -} - -/* Runtime schema lookup — request schemas by method */ -type RequestSchemaType = (typeof ClientRequestSchema.options)[number] | (typeof ServerRequestSchema.options)[number]; -type NotificationSchemaType = (typeof ClientNotificationSchema.options)[number] | (typeof ServerNotificationSchema.options)[number]; - -function buildSchemaMap(schemas: readonly T[]): Record { - const map: Record = {}; - for (const schema of schemas) { - const method = schema.shape.method.value; - map[method] = schema; - } - return map; -} - -const requestSchemas = buildSchemaMap([...ClientRequestSchema.options, ...ServerRequestSchema.options] as const) as Record< - RequestMethod, - RequestSchemaType ->; -const notificationSchemas = buildSchemaMap([...ClientNotificationSchema.options, ...ServerNotificationSchema.options] as const) as Record< - NotificationMethod, - NotificationSchemaType ->; - -/** - * Gets the Zod schema for a given request method. - * Returns `undefined` for non-spec methods. - * The return type is a ZodType that parses to RequestTypeMap[M], allowing callers - * to use schema.parse() without needing additional type assertions. - * - * Note: The internal cast is necessary because TypeScript can't correlate the - * Record-based schema lookup with the MethodToTypeMap-based RequestTypeMap - * when M is a generic type parameter. Both compute to the same type at - * instantiation, but TypeScript can't prove this statically. - */ -export function getRequestSchema(method: M): z.ZodType; -export function getRequestSchema(method: string): z.ZodType | undefined; -export function getRequestSchema(method: string): z.ZodType | undefined { - return requestSchemas[method as RequestMethod] as unknown as z.ZodType | undefined; -} - -/** - * Gets the Zod schema for a given notification method. - * Returns `undefined` for non-spec methods. - * @see getRequestSchema for explanation of the internal type assertion. - */ -export function getNotificationSchema(method: M): z.ZodType; -export function getNotificationSchema(method: string): z.ZodType | undefined; -export function getNotificationSchema(method: string): z.ZodType | undefined { - return notificationSchemas[method as NotificationMethod] as unknown as z.ZodType | undefined; -} diff --git a/packages/core/src/types/spec.types.2025-11-25.ts b/packages/core/src/types/spec.types.2025-11-25.ts deleted file mode 100644 index 225a53c2d7..0000000000 --- a/packages/core/src/types/spec.types.2025-11-25.ts +++ /dev/null @@ -1,2559 +0,0 @@ -/** - * This file is automatically generated from the Model Context Protocol specification. - * - * Source: https://github.com/modelcontextprotocol/modelcontextprotocol - * Pulled from: https://raw.githubusercontent.com/modelcontextprotocol/modelcontextprotocol/main/schema/2025-11-25/schema.ts - * Last updated from commit: 357adac47ab2654b64799f994e6db8d3df4ee19d - * - * DO NOT EDIT THIS FILE MANUALLY. Changes will be overwritten by automated updates. - * To update this file, run: pnpm run fetch:spec-types 2025-11-25 - */ /* JSON-RPC types */ - -/** - * Refers to any valid JSON-RPC object that can be decoded off the wire, or encoded to be sent. - * - * @category JSON-RPC - */ -export type JSONRPCMessage = JSONRPCRequest | JSONRPCNotification | JSONRPCResponse; - -/** @internal */ -export const LATEST_PROTOCOL_VERSION = '2025-11-25'; -/** @internal */ -export const JSONRPC_VERSION = '2.0'; - -/** - * A progress token, used to associate progress notifications with the original request. - * - * @category Common Types - */ -export type ProgressToken = string | number; - -/** - * An opaque token used to represent a cursor for pagination. - * - * @category Common Types - */ -export type Cursor = string; - -/** - * Common params for any task-augmented request. - * - * @internal - */ -export interface TaskAugmentedRequestParams extends RequestParams { - /** - * If specified, the caller is requesting task-augmented execution for this request. - * The request will return a CreateTaskResult immediately, and the actual result can be - * retrieved later via tasks/result. - * - * Task augmentation is subject to capability negotiation - receivers MUST declare support - * for task augmentation of specific request types in their capabilities. - */ - task?: TaskMetadata; -} -/** - * Common params for any request. - * - * @internal - */ -export interface RequestParams { - /** - * See [General fields: `_meta`](/specification/2025-11-25/basic/index#meta) for notes on `_meta` usage. - */ - _meta?: { - /** - * If specified, the caller is requesting out-of-band progress notifications for this request (as represented by notifications/progress). The value of this parameter is an opaque token that will be attached to any subsequent notifications. The receiver is not obligated to provide these notifications. - */ - progressToken?: ProgressToken; - [key: string]: unknown; - }; -} - -/** @internal */ -export interface Request { - method: string; - // Allow unofficial extensions of `Request.params` without impacting `RequestParams`. - // eslint-disable-next-line @typescript-eslint/no-explicit-any - params?: { [key: string]: any }; -} - -/** @internal */ -export interface NotificationParams { - /** - * See [General fields: `_meta`](/specification/2025-11-25/basic/index#meta) for notes on `_meta` usage. - */ - _meta?: { [key: string]: unknown }; -} - -/** @internal */ -export interface Notification { - method: string; - // Allow unofficial extensions of `Notification.params` without impacting `NotificationParams`. - // eslint-disable-next-line @typescript-eslint/no-explicit-any - params?: { [key: string]: any }; -} - -/** - * @category Common Types - */ -export interface Result { - /** - * See [General fields: `_meta`](/specification/2025-11-25/basic/index#meta) for notes on `_meta` usage. - */ - _meta?: { [key: string]: unknown }; - [key: string]: unknown; -} - -/** - * @category Common Types - */ -export interface Error { - /** - * The error type that occurred. - */ - code: number; - /** - * A short description of the error. The message SHOULD be limited to a concise single sentence. - */ - message: string; - /** - * Additional information about the error. The value of this member is defined by the sender (e.g. detailed error information, nested errors etc.). - */ - data?: unknown; -} - -/** - * A uniquely identifying ID for a request in JSON-RPC. - * - * @category Common Types - */ -export type RequestId = string | number; - -/** - * A request that expects a response. - * - * @category JSON-RPC - */ -export interface JSONRPCRequest extends Request { - jsonrpc: typeof JSONRPC_VERSION; - id: RequestId; -} - -/** - * A notification which does not expect a response. - * - * @category JSON-RPC - */ -export interface JSONRPCNotification extends Notification { - jsonrpc: typeof JSONRPC_VERSION; -} - -/** - * A successful (non-error) response to a request. - * - * @category JSON-RPC - */ -export interface JSONRPCResultResponse { - jsonrpc: typeof JSONRPC_VERSION; - id: RequestId; - result: Result; -} - -/** - * A response to a request that indicates an error occurred. - * - * @category JSON-RPC - */ -export interface JSONRPCErrorResponse { - jsonrpc: typeof JSONRPC_VERSION; - id?: RequestId; - error: Error; -} - -/** - * A response to a request, containing either the result or error. - * - * @category JSON-RPC - */ -export type JSONRPCResponse = JSONRPCResultResponse | JSONRPCErrorResponse; - -// Standard JSON-RPC error codes -export const PARSE_ERROR = -32700; -export const INVALID_REQUEST = -32600; -export const METHOD_NOT_FOUND = -32601; -export const INVALID_PARAMS = -32602; -export const INTERNAL_ERROR = -32603; - -// Implementation-specific JSON-RPC error codes [-32000, -32099] -/** @internal */ -export const URL_ELICITATION_REQUIRED = -32042; - -/** - * An error response that indicates that the server requires the client to provide additional information via an elicitation request. - * - * @internal - */ -export interface URLElicitationRequiredError extends Omit { - error: Error & { - code: typeof URL_ELICITATION_REQUIRED; - data: { - elicitations: ElicitRequestURLParams[]; - [key: string]: unknown; - }; - }; -} - -/* Empty result */ -/** - * A response that indicates success but carries no data. - * - * @category Common Types - */ -export type EmptyResult = Result; - -/* Cancellation */ -/** - * Parameters for a `notifications/cancelled` notification. - * - * @category `notifications/cancelled` - */ -export interface CancelledNotificationParams extends NotificationParams { - /** - * The ID of the request to cancel. - * - * This MUST correspond to the ID of a request previously issued in the same direction. - * This MUST be provided for cancelling non-task requests. - * This MUST NOT be used for cancelling tasks (use the `tasks/cancel` request instead). - */ - requestId?: RequestId; - - /** - * An optional string describing the reason for the cancellation. This MAY be logged or presented to the user. - */ - reason?: string; -} - -/** - * This notification can be sent by either side to indicate that it is cancelling a previously-issued request. - * - * The request SHOULD still be in-flight, but due to communication latency, it is always possible that this notification MAY arrive after the request has already finished. - * - * This notification indicates that the result will be unused, so any associated processing SHOULD cease. - * - * A client MUST NOT attempt to cancel its `initialize` request. - * - * For task cancellation, use the `tasks/cancel` request instead of this notification. - * - * @category `notifications/cancelled` - */ -export interface CancelledNotification extends JSONRPCNotification { - method: 'notifications/cancelled'; - params: CancelledNotificationParams; -} - -/* Initialization */ -/** - * Parameters for an `initialize` request. - * - * @category `initialize` - */ -export interface InitializeRequestParams extends RequestParams { - /** - * The latest version of the Model Context Protocol that the client supports. The client MAY decide to support older versions as well. - */ - protocolVersion: string; - capabilities: ClientCapabilities; - clientInfo: Implementation; -} - -/** - * This request is sent from the client to the server when it first connects, asking it to begin initialization. - * - * @category `initialize` - */ -export interface InitializeRequest extends JSONRPCRequest { - method: 'initialize'; - params: InitializeRequestParams; -} - -/** - * After receiving an initialize request from the client, the server sends this response. - * - * @category `initialize` - */ -export interface InitializeResult extends Result { - /** - * The version of the Model Context Protocol that the server wants to use. This may not match the version that the client requested. If the client cannot support this version, it MUST disconnect. - */ - protocolVersion: string; - capabilities: ServerCapabilities; - serverInfo: Implementation; - - /** - * Instructions describing how to use the server and its features. - * - * This can be used by clients to improve the LLM's understanding of available tools, resources, etc. It can be thought of like a "hint" to the model. For example, this information MAY be added to the system prompt. - */ - instructions?: string; -} - -/** - * This notification is sent from the client to the server after initialization has finished. - * - * @category `notifications/initialized` - */ -export interface InitializedNotification extends JSONRPCNotification { - method: 'notifications/initialized'; - params?: NotificationParams; -} - -/** - * Capabilities a client may support. Known capabilities are defined here, in this schema, but this is not a closed set: any client can define its own, additional capabilities. - * - * @category `initialize` - */ -export interface ClientCapabilities { - /** - * Experimental, non-standard capabilities that the client supports. - */ - experimental?: { [key: string]: object }; - /** - * Present if the client supports listing roots. - */ - roots?: { - /** - * Whether the client supports notifications for changes to the roots list. - */ - listChanged?: boolean; - }; - /** - * Present if the client supports sampling from an LLM. - */ - sampling?: { - /** - * Whether the client supports context inclusion via includeContext parameter. - * If not declared, servers SHOULD only use `includeContext: "none"` (or omit it). - */ - context?: object; - /** - * Whether the client supports tool use via tools and toolChoice parameters. - */ - tools?: object; - }; - /** - * Present if the client supports elicitation from the server. - */ - elicitation?: { form?: object; url?: object }; - - /** - * Present if the client supports task-augmented requests. - */ - tasks?: { - /** - * Whether this client supports tasks/list. - */ - list?: object; - /** - * Whether this client supports tasks/cancel. - */ - cancel?: object; - /** - * Specifies which request types can be augmented with tasks. - */ - requests?: { - /** - * Task support for sampling-related requests. - */ - sampling?: { - /** - * Whether the client supports task-augmented sampling/createMessage requests. - */ - createMessage?: object; - }; - /** - * Task support for elicitation-related requests. - */ - elicitation?: { - /** - * Whether the client supports task-augmented elicitation/create requests. - */ - create?: object; - }; - }; - }; -} - -/** - * Capabilities that a server may support. Known capabilities are defined here, in this schema, but this is not a closed set: any server can define its own, additional capabilities. - * - * @category `initialize` - */ -export interface ServerCapabilities { - /** - * Experimental, non-standard capabilities that the server supports. - */ - experimental?: { [key: string]: object }; - /** - * Present if the server supports sending log messages to the client. - */ - logging?: object; - /** - * Present if the server supports argument autocompletion suggestions. - */ - completions?: object; - /** - * Present if the server offers any prompt templates. - */ - prompts?: { - /** - * Whether this server supports notifications for changes to the prompt list. - */ - listChanged?: boolean; - }; - /** - * Present if the server offers any resources to read. - */ - resources?: { - /** - * Whether this server supports subscribing to resource updates. - */ - subscribe?: boolean; - /** - * Whether this server supports notifications for changes to the resource list. - */ - listChanged?: boolean; - }; - /** - * Present if the server offers any tools to call. - */ - tools?: { - /** - * Whether this server supports notifications for changes to the tool list. - */ - listChanged?: boolean; - }; - /** - * Present if the server supports task-augmented requests. - */ - tasks?: { - /** - * Whether this server supports tasks/list. - */ - list?: object; - /** - * Whether this server supports tasks/cancel. - */ - cancel?: object; - /** - * Specifies which request types can be augmented with tasks. - */ - requests?: { - /** - * Task support for tool-related requests. - */ - tools?: { - /** - * Whether the server supports task-augmented tools/call requests. - */ - call?: object; - }; - }; - }; -} - -/** - * An optionally-sized icon that can be displayed in a user interface. - * - * @category Common Types - */ -export interface Icon { - /** - * A standard URI pointing to an icon resource. May be an HTTP/HTTPS URL or a - * `data:` URI with Base64-encoded image data. - * - * Consumers SHOULD takes steps to ensure URLs serving icons are from the - * same domain as the client/server or a trusted domain. - * - * Consumers SHOULD take appropriate precautions when consuming SVGs as they can contain - * executable JavaScript. - * - * @format uri - */ - src: string; - - /** - * Optional MIME type override if the source MIME type is missing or generic. - * For example: `"image/png"`, `"image/jpeg"`, or `"image/svg+xml"`. - */ - mimeType?: string; - - /** - * Optional array of strings that specify sizes at which the icon can be used. - * Each string should be in WxH format (e.g., `"48x48"`, `"96x96"`) or `"any"` for scalable formats like SVG. - * - * If not provided, the client should assume that the icon can be used at any size. - */ - sizes?: string[]; - - /** - * Optional specifier for the theme this icon is designed for. `light` indicates - * the icon is designed to be used with a light background, and `dark` indicates - * the icon is designed to be used with a dark background. - * - * If not provided, the client should assume the icon can be used with any theme. - */ - theme?: 'light' | 'dark'; -} - -/** - * Base interface to add `icons` property. - * - * @internal - */ -export interface Icons { - /** - * Optional set of sized icons that the client can display in a user interface. - * - * Clients that support rendering icons MUST support at least the following MIME types: - * - `image/png` - PNG images (safe, universal compatibility) - * - `image/jpeg` (and `image/jpg`) - JPEG images (safe, universal compatibility) - * - * Clients that support rendering icons SHOULD also support: - * - `image/svg+xml` - SVG images (scalable but requires security precautions) - * - `image/webp` - WebP images (modern, efficient format) - */ - icons?: Icon[]; -} - -/** - * Base interface for metadata with name (identifier) and title (display name) properties. - * - * @internal - */ -export interface BaseMetadata { - /** - * Intended for programmatic or logical use, but used as a display name in past specs or fallback (if title isn't present). - */ - name: string; - - /** - * Intended for UI and end-user contexts — optimized to be human-readable and easily understood, - * even by those unfamiliar with domain-specific terminology. - * - * If not provided, the name should be used for display (except for Tool, - * where `annotations.title` should be given precedence over using `name`, - * if present). - */ - title?: string; -} - -/** - * Describes the MCP implementation. - * - * @category `initialize` - */ -export interface Implementation extends BaseMetadata, Icons { - version: string; - - /** - * An optional human-readable description of what this implementation does. - * - * This can be used by clients or servers to provide context about their purpose - * and capabilities. For example, a server might describe the types of resources - * or tools it provides, while a client might describe its intended use case. - */ - description?: string; - - /** - * An optional URL of the website for this implementation. - * - * @format uri - */ - websiteUrl?: string; -} - -/* Ping */ -/** - * A ping, issued by either the server or the client, to check that the other party is still alive. The receiver must promptly respond, or else may be disconnected. - * - * @category `ping` - */ -export interface PingRequest extends JSONRPCRequest { - method: 'ping'; - params?: RequestParams; -} - -/* Progress notifications */ - -/** - * Parameters for a `notifications/progress` notification. - * - * @category `notifications/progress` - */ -export interface ProgressNotificationParams extends NotificationParams { - /** - * The progress token which was given in the initial request, used to associate this notification with the request that is proceeding. - */ - progressToken: ProgressToken; - /** - * The progress thus far. This should increase every time progress is made, even if the total is unknown. - * - * @TJS-type number - */ - progress: number; - /** - * Total number of items to process (or total progress required), if known. - * - * @TJS-type number - */ - total?: number; - /** - * An optional message describing the current progress. - */ - message?: string; -} - -/** - * An out-of-band notification used to inform the receiver of a progress update for a long-running request. - * - * @category `notifications/progress` - */ -export interface ProgressNotification extends JSONRPCNotification { - method: 'notifications/progress'; - params: ProgressNotificationParams; -} - -/* Pagination */ -/** - * Common parameters for paginated requests. - * - * @internal - */ -export interface PaginatedRequestParams extends RequestParams { - /** - * An opaque token representing the current pagination position. - * If provided, the server should return results starting after this cursor. - */ - cursor?: Cursor; -} - -/** @internal */ -export interface PaginatedRequest extends JSONRPCRequest { - params?: PaginatedRequestParams; -} - -/** @internal */ -export interface PaginatedResult extends Result { - /** - * An opaque token representing the pagination position after the last returned result. - * If present, there may be more results available. - */ - nextCursor?: Cursor; -} - -/* Resources */ -/** - * Sent from the client to request a list of resources the server has. - * - * @category `resources/list` - */ -export interface ListResourcesRequest extends PaginatedRequest { - method: 'resources/list'; -} - -/** - * The server's response to a resources/list request from the client. - * - * @category `resources/list` - */ -export interface ListResourcesResult extends PaginatedResult { - resources: Resource[]; -} - -/** - * Sent from the client to request a list of resource templates the server has. - * - * @category `resources/templates/list` - */ -export interface ListResourceTemplatesRequest extends PaginatedRequest { - method: 'resources/templates/list'; -} - -/** - * The server's response to a resources/templates/list request from the client. - * - * @category `resources/templates/list` - */ -export interface ListResourceTemplatesResult extends PaginatedResult { - resourceTemplates: ResourceTemplate[]; -} - -/** - * Common parameters when working with resources. - * - * @internal - */ -export interface ResourceRequestParams extends RequestParams { - /** - * The URI of the resource. The URI can use any protocol; it is up to the server how to interpret it. - * - * @format uri - */ - uri: string; -} - -/** - * Parameters for a `resources/read` request. - * - * @category `resources/read` - */ -// eslint-disable-next-line @typescript-eslint/no-empty-object-type -export interface ReadResourceRequestParams extends ResourceRequestParams {} - -/** - * Sent from the client to the server, to read a specific resource URI. - * - * @category `resources/read` - */ -export interface ReadResourceRequest extends JSONRPCRequest { - method: 'resources/read'; - params: ReadResourceRequestParams; -} - -/** - * The server's response to a resources/read request from the client. - * - * @category `resources/read` - */ -export interface ReadResourceResult extends Result { - contents: (TextResourceContents | BlobResourceContents)[]; -} - -/** - * An optional notification from the server to the client, informing it that the list of resources it can read from has changed. This may be issued by servers without any previous subscription from the client. - * - * @category `notifications/resources/list_changed` - */ -export interface ResourceListChangedNotification extends JSONRPCNotification { - method: 'notifications/resources/list_changed'; - params?: NotificationParams; -} - -/** - * Parameters for a `resources/subscribe` request. - * - * @category `resources/subscribe` - */ -// eslint-disable-next-line @typescript-eslint/no-empty-object-type -export interface SubscribeRequestParams extends ResourceRequestParams {} - -/** - * Sent from the client to request resources/updated notifications from the server whenever a particular resource changes. - * - * @category `resources/subscribe` - */ -export interface SubscribeRequest extends JSONRPCRequest { - method: 'resources/subscribe'; - params: SubscribeRequestParams; -} - -/** - * Parameters for a `resources/unsubscribe` request. - * - * @category `resources/unsubscribe` - */ -// eslint-disable-next-line @typescript-eslint/no-empty-object-type -export interface UnsubscribeRequestParams extends ResourceRequestParams {} - -/** - * Sent from the client to request cancellation of resources/updated notifications from the server. This should follow a previous resources/subscribe request. - * - * @category `resources/unsubscribe` - */ -export interface UnsubscribeRequest extends JSONRPCRequest { - method: 'resources/unsubscribe'; - params: UnsubscribeRequestParams; -} - -/** - * Parameters for a `notifications/resources/updated` notification. - * - * @category `notifications/resources/updated` - */ -export interface ResourceUpdatedNotificationParams extends NotificationParams { - /** - * The URI of the resource that has been updated. This might be a sub-resource of the one that the client actually subscribed to. - * - * @format uri - */ - uri: string; -} - -/** - * A notification from the server to the client, informing it that a resource has changed and may need to be read again. This should only be sent if the client previously sent a resources/subscribe request. - * - * @category `notifications/resources/updated` - */ -export interface ResourceUpdatedNotification extends JSONRPCNotification { - method: 'notifications/resources/updated'; - params: ResourceUpdatedNotificationParams; -} - -/** - * A known resource that the server is capable of reading. - * - * @category `resources/list` - */ -export interface Resource extends BaseMetadata, Icons { - /** - * The URI of this resource. - * - * @format uri - */ - uri: string; - - /** - * A description of what this resource represents. - * - * This can be used by clients to improve the LLM's understanding of available resources. It can be thought of like a "hint" to the model. - */ - description?: string; - - /** - * The MIME type of this resource, if known. - */ - mimeType?: string; - - /** - * Optional annotations for the client. - */ - annotations?: Annotations; - - /** - * The size of the raw resource content, in bytes (i.e., before base64 encoding or any tokenization), if known. - * - * This can be used by Hosts to display file sizes and estimate context window usage. - */ - size?: number; - - /** - * See [General fields: `_meta`](/specification/2025-11-25/basic/index#meta) for notes on `_meta` usage. - */ - _meta?: { [key: string]: unknown }; -} - -/** - * A template description for resources available on the server. - * - * @category `resources/templates/list` - */ -export interface ResourceTemplate extends BaseMetadata, Icons { - /** - * A URI template (according to RFC 6570) that can be used to construct resource URIs. - * - * @format uri-template - */ - uriTemplate: string; - - /** - * A description of what this template is for. - * - * This can be used by clients to improve the LLM's understanding of available resources. It can be thought of like a "hint" to the model. - */ - description?: string; - - /** - * The MIME type for all resources that match this template. This should only be included if all resources matching this template have the same type. - */ - mimeType?: string; - - /** - * Optional annotations for the client. - */ - annotations?: Annotations; - - /** - * See [General fields: `_meta`](/specification/2025-11-25/basic/index#meta) for notes on `_meta` usage. - */ - _meta?: { [key: string]: unknown }; -} - -/** - * The contents of a specific resource or sub-resource. - * - * @internal - */ -export interface ResourceContents { - /** - * The URI of this resource. - * - * @format uri - */ - uri: string; - /** - * The MIME type of this resource, if known. - */ - mimeType?: string; - - /** - * See [General fields: `_meta`](/specification/2025-11-25/basic/index#meta) for notes on `_meta` usage. - */ - _meta?: { [key: string]: unknown }; -} - -/** - * @category Content - */ -export interface TextResourceContents extends ResourceContents { - /** - * The text of the item. This must only be set if the item can actually be represented as text (not binary data). - */ - text: string; -} - -/** - * @category Content - */ -export interface BlobResourceContents extends ResourceContents { - /** - * A base64-encoded string representing the binary data of the item. - * - * @format byte - */ - blob: string; -} - -/* Prompts */ -/** - * Sent from the client to request a list of prompts and prompt templates the server has. - * - * @category `prompts/list` - */ -export interface ListPromptsRequest extends PaginatedRequest { - method: 'prompts/list'; -} - -/** - * The server's response to a prompts/list request from the client. - * - * @category `prompts/list` - */ -export interface ListPromptsResult extends PaginatedResult { - prompts: Prompt[]; -} - -/** - * Parameters for a `prompts/get` request. - * - * @category `prompts/get` - */ -export interface GetPromptRequestParams extends RequestParams { - /** - * The name of the prompt or prompt template. - */ - name: string; - /** - * Arguments to use for templating the prompt. - */ - arguments?: { [key: string]: string }; -} - -/** - * Used by the client to get a prompt provided by the server. - * - * @category `prompts/get` - */ -export interface GetPromptRequest extends JSONRPCRequest { - method: 'prompts/get'; - params: GetPromptRequestParams; -} - -/** - * The server's response to a prompts/get request from the client. - * - * @category `prompts/get` - */ -export interface GetPromptResult extends Result { - /** - * An optional description for the prompt. - */ - description?: string; - messages: PromptMessage[]; -} - -/** - * A prompt or prompt template that the server offers. - * - * @category `prompts/list` - */ -export interface Prompt extends BaseMetadata, Icons { - /** - * An optional description of what this prompt provides - */ - description?: string; - - /** - * A list of arguments to use for templating the prompt. - */ - arguments?: PromptArgument[]; - - /** - * See [General fields: `_meta`](/specification/2025-11-25/basic/index#meta) for notes on `_meta` usage. - */ - _meta?: { [key: string]: unknown }; -} - -/** - * Describes an argument that a prompt can accept. - * - * @category `prompts/list` - */ -export interface PromptArgument extends BaseMetadata { - /** - * A human-readable description of the argument. - */ - description?: string; - /** - * Whether this argument must be provided. - */ - required?: boolean; -} - -/** - * The sender or recipient of messages and data in a conversation. - * - * @category Common Types - */ -export type Role = 'user' | 'assistant'; - -/** - * Describes a message returned as part of a prompt. - * - * This is similar to `SamplingMessage`, but also supports the embedding of - * resources from the MCP server. - * - * @category `prompts/get` - */ -export interface PromptMessage { - role: Role; - content: ContentBlock; -} - -/** - * A resource that the server is capable of reading, included in a prompt or tool call result. - * - * Note: resource links returned by tools are not guaranteed to appear in the results of `resources/list` requests. - * - * @category Content - */ -export interface ResourceLink extends Resource { - type: 'resource_link'; -} - -/** - * The contents of a resource, embedded into a prompt or tool call result. - * - * It is up to the client how best to render embedded resources for the benefit - * of the LLM and/or the user. - * - * @category Content - */ -export interface EmbeddedResource { - type: 'resource'; - resource: TextResourceContents | BlobResourceContents; - - /** - * Optional annotations for the client. - */ - annotations?: Annotations; - - /** - * See [General fields: `_meta`](/specification/2025-11-25/basic/index#meta) for notes on `_meta` usage. - */ - _meta?: { [key: string]: unknown }; -} -/** - * An optional notification from the server to the client, informing it that the list of prompts it offers has changed. This may be issued by servers without any previous subscription from the client. - * - * @category `notifications/prompts/list_changed` - */ -export interface PromptListChangedNotification extends JSONRPCNotification { - method: 'notifications/prompts/list_changed'; - params?: NotificationParams; -} - -/* Tools */ -/** - * Sent from the client to request a list of tools the server has. - * - * @category `tools/list` - */ -export interface ListToolsRequest extends PaginatedRequest { - method: 'tools/list'; -} - -/** - * The server's response to a tools/list request from the client. - * - * @category `tools/list` - */ -export interface ListToolsResult extends PaginatedResult { - tools: Tool[]; -} - -/** - * The server's response to a tool call. - * - * @category `tools/call` - */ -export interface CallToolResult extends Result { - /** - * A list of content objects that represent the unstructured result of the tool call. - */ - content: ContentBlock[]; - - /** - * An optional JSON object that represents the structured result of the tool call. - */ - structuredContent?: { [key: string]: unknown }; - - /** - * Whether the tool call ended in an error. - * - * If not set, this is assumed to be false (the call was successful). - * - * Any errors that originate from the tool SHOULD be reported inside the result - * object, with `isError` set to true, _not_ as an MCP protocol-level error - * response. Otherwise, the LLM would not be able to see that an error occurred - * and self-correct. - * - * However, any errors in _finding_ the tool, an error indicating that the - * server does not support tool calls, or any other exceptional conditions, - * should be reported as an MCP error response. - */ - isError?: boolean; -} - -/** - * Parameters for a `tools/call` request. - * - * @category `tools/call` - */ -export interface CallToolRequestParams extends TaskAugmentedRequestParams { - /** - * The name of the tool. - */ - name: string; - /** - * Arguments to use for the tool call. - */ - arguments?: { [key: string]: unknown }; -} - -/** - * Used by the client to invoke a tool provided by the server. - * - * @category `tools/call` - */ -export interface CallToolRequest extends JSONRPCRequest { - method: 'tools/call'; - params: CallToolRequestParams; -} - -/** - * An optional notification from the server to the client, informing it that the list of tools it offers has changed. This may be issued by servers without any previous subscription from the client. - * - * @category `notifications/tools/list_changed` - */ -export interface ToolListChangedNotification extends JSONRPCNotification { - method: 'notifications/tools/list_changed'; - params?: NotificationParams; -} - -/** - * Additional properties describing a Tool to clients. - * - * NOTE: all properties in ToolAnnotations are **hints**. - * They are not guaranteed to provide a faithful description of - * tool behavior (including descriptive properties like `title`). - * - * Clients should never make tool use decisions based on ToolAnnotations - * received from untrusted servers. - * - * @category `tools/list` - */ -export interface ToolAnnotations { - /** - * A human-readable title for the tool. - */ - title?: string; - - /** - * If true, the tool does not modify its environment. - * - * Default: false - */ - readOnlyHint?: boolean; - - /** - * If true, the tool may perform destructive updates to its environment. - * If false, the tool performs only additive updates. - * - * (This property is meaningful only when `readOnlyHint == false`) - * - * Default: true - */ - destructiveHint?: boolean; - - /** - * If true, calling the tool repeatedly with the same arguments - * will have no additional effect on its environment. - * - * (This property is meaningful only when `readOnlyHint == false`) - * - * Default: false - */ - idempotentHint?: boolean; - - /** - * If true, this tool may interact with an "open world" of external - * entities. If false, the tool's domain of interaction is closed. - * For example, the world of a web search tool is open, whereas that - * of a memory tool is not. - * - * Default: true - */ - openWorldHint?: boolean; -} - -/** - * Execution-related properties for a tool. - * - * @category `tools/list` - */ -export interface ToolExecution { - /** - * Indicates whether this tool supports task-augmented execution. - * This allows clients to handle long-running operations through polling - * the task system. - * - * - "forbidden": Tool does not support task-augmented execution (default when absent) - * - "optional": Tool may support task-augmented execution - * - "required": Tool requires task-augmented execution - * - * Default: "forbidden" - */ - taskSupport?: 'forbidden' | 'optional' | 'required'; -} - -/** - * Definition for a tool the client can call. - * - * @category `tools/list` - */ -export interface Tool extends BaseMetadata, Icons { - /** - * A human-readable description of the tool. - * - * This can be used by clients to improve the LLM's understanding of available tools. It can be thought of like a "hint" to the model. - */ - description?: string; - - /** - * A JSON Schema object defining the expected parameters for the tool. - */ - inputSchema: { - $schema?: string; - type: 'object'; - properties?: { [key: string]: object }; - required?: string[]; - }; - - /** - * Execution-related properties for this tool. - */ - execution?: ToolExecution; - - /** - * An optional JSON Schema object defining the structure of the tool's output returned in - * the structuredContent field of a CallToolResult. - * - * Defaults to JSON Schema 2020-12 when no explicit $schema is provided. - * Currently restricted to type: "object" at the root level. - */ - outputSchema?: { - $schema?: string; - type: 'object'; - properties?: { [key: string]: object }; - required?: string[]; - }; - - /** - * Optional additional tool information. - * - * Display name precedence order is: title, annotations.title, then name. - */ - annotations?: ToolAnnotations; - - /** - * See [General fields: `_meta`](/specification/2025-11-25/basic/index#meta) for notes on `_meta` usage. - */ - _meta?: { [key: string]: unknown }; -} - -/* Tasks */ - -/** - * The status of a task. - * - * @category `tasks` - */ -export type TaskStatus = - | 'working' // The request is currently being processed - | 'input_required' // The task is waiting for input (e.g., elicitation or sampling) - | 'completed' // The request completed successfully and results are available - | 'failed' // The associated request did not complete successfully. For tool calls specifically, this includes cases where the tool call result has `isError` set to true. - | 'cancelled'; // The request was cancelled before completion - -/** - * Metadata for augmenting a request with task execution. - * Include this in the `task` field of the request parameters. - * - * @category `tasks` - */ -export interface TaskMetadata { - /** - * Requested duration in milliseconds to retain task from creation. - */ - ttl?: number; -} - -/** - * Metadata for associating messages with a task. - * Include this in the `_meta` field under the key `io.modelcontextprotocol/related-task`. - * - * @category `tasks` - */ -export interface RelatedTaskMetadata { - /** - * The task identifier this message is associated with. - */ - taskId: string; -} - -/** - * Data associated with a task. - * - * @category `tasks` - */ -export interface Task { - /** - * The task identifier. - */ - taskId: string; - - /** - * Current task state. - */ - status: TaskStatus; - - /** - * Optional human-readable message describing the current task state. - * This can provide context for any status, including: - * - Reasons for "cancelled" status - * - Summaries for "completed" status - * - Diagnostic information for "failed" status (e.g., error details, what went wrong) - */ - statusMessage?: string; - - /** - * ISO 8601 timestamp when the task was created. - */ - createdAt: string; - - /** - * ISO 8601 timestamp when the task was last updated. - */ - lastUpdatedAt: string; - - /** - * Actual retention duration from creation in milliseconds, null for unlimited. - * @nullable - */ - ttl: number | null; - - /** - * Suggested polling interval in milliseconds. - */ - pollInterval?: number; -} - -/** - * A response to a task-augmented request. - * - * @category `tasks` - */ -export interface CreateTaskResult extends Result { - task: Task; -} - -/** - * A request to retrieve the state of a task. - * - * @category `tasks/get` - */ -export interface GetTaskRequest extends JSONRPCRequest { - method: 'tasks/get'; - params: { - /** - * The task identifier to query. - */ - taskId: string; - }; -} - -/** - * The response to a tasks/get request. - * - * @category `tasks/get` - */ -export type GetTaskResult = Result & Task; - -/** - * A request to retrieve the result of a completed task. - * - * @category `tasks/result` - */ -export interface GetTaskPayloadRequest extends JSONRPCRequest { - method: 'tasks/result'; - params: { - /** - * The task identifier to retrieve results for. - */ - taskId: string; - }; -} - -/** - * The response to a tasks/result request. - * The structure matches the result type of the original request. - * For example, a tools/call task would return the CallToolResult structure. - * - * @category `tasks/result` - */ -export interface GetTaskPayloadResult extends Result { - [key: string]: unknown; -} - -/** - * A request to cancel a task. - * - * @category `tasks/cancel` - */ -export interface CancelTaskRequest extends JSONRPCRequest { - method: 'tasks/cancel'; - params: { - /** - * The task identifier to cancel. - */ - taskId: string; - }; -} - -/** - * The response to a tasks/cancel request. - * - * @category `tasks/cancel` - */ -export type CancelTaskResult = Result & Task; - -/** - * A request to retrieve a list of tasks. - * - * @category `tasks/list` - */ -export interface ListTasksRequest extends PaginatedRequest { - method: 'tasks/list'; -} - -/** - * The response to a tasks/list request. - * - * @category `tasks/list` - */ -export interface ListTasksResult extends PaginatedResult { - tasks: Task[]; -} - -/** - * Parameters for a `notifications/tasks/status` notification. - * - * @category `notifications/tasks/status` - */ -export type TaskStatusNotificationParams = NotificationParams & Task; - -/** - * An optional notification from the receiver to the requestor, informing them that a task's status has changed. Receivers are not required to send these notifications. - * - * @category `notifications/tasks/status` - */ -export interface TaskStatusNotification extends JSONRPCNotification { - method: 'notifications/tasks/status'; - params: TaskStatusNotificationParams; -} - -/* Logging */ - -/** - * Parameters for a `logging/setLevel` request. - * - * @category `logging/setLevel` - */ -export interface SetLevelRequestParams extends RequestParams { - /** - * The level of logging that the client wants to receive from the server. The server should send all logs at this level and higher (i.e., more severe) to the client as notifications/message. - */ - level: LoggingLevel; -} - -/** - * A request from the client to the server, to enable or adjust logging. - * - * @category `logging/setLevel` - */ -export interface SetLevelRequest extends JSONRPCRequest { - method: 'logging/setLevel'; - params: SetLevelRequestParams; -} - -/** - * Parameters for a `notifications/message` notification. - * - * @category `notifications/message` - */ -export interface LoggingMessageNotificationParams extends NotificationParams { - /** - * The severity of this log message. - */ - level: LoggingLevel; - /** - * An optional name of the logger issuing this message. - */ - logger?: string; - /** - * The data to be logged, such as a string message or an object. Any JSON serializable type is allowed here. - */ - data: unknown; -} - -/** - * JSONRPCNotification of a log message passed from server to client. If no logging/setLevel request has been sent from the client, the server MAY decide which messages to send automatically. - * - * @category `notifications/message` - */ -export interface LoggingMessageNotification extends JSONRPCNotification { - method: 'notifications/message'; - params: LoggingMessageNotificationParams; -} - -/** - * The severity of a log message. - * - * These map to syslog message severities, as specified in RFC-5424: - * https://datatracker.ietf.org/doc/html/rfc5424#section-6.2.1 - * - * @category Common Types - */ -export type LoggingLevel = 'debug' | 'info' | 'notice' | 'warning' | 'error' | 'critical' | 'alert' | 'emergency'; - -/* Sampling */ -/** - * Parameters for a `sampling/createMessage` request. - * - * @category `sampling/createMessage` - */ -export interface CreateMessageRequestParams extends TaskAugmentedRequestParams { - messages: SamplingMessage[]; - /** - * The server's preferences for which model to select. The client MAY ignore these preferences. - */ - modelPreferences?: ModelPreferences; - /** - * An optional system prompt the server wants to use for sampling. The client MAY modify or omit this prompt. - */ - systemPrompt?: string; - /** - * A request to include context from one or more MCP servers (including the caller), to be attached to the prompt. - * The client MAY ignore this request. - * - * Default is "none". Values "thisServer" and "allServers" are soft-deprecated. Servers SHOULD only use these values if the client - * declares ClientCapabilities.sampling.context. These values may be removed in future spec releases. - */ - includeContext?: 'none' | 'thisServer' | 'allServers'; - /** - * @TJS-type number - */ - temperature?: number; - /** - * The requested maximum number of tokens to sample (to prevent runaway completions). - * - * The client MAY choose to sample fewer tokens than the requested maximum. - */ - maxTokens: number; - stopSequences?: string[]; - /** - * Optional metadata to pass through to the LLM provider. The format of this metadata is provider-specific. - */ - metadata?: object; - /** - * Tools that the model may use during generation. - * The client MUST return an error if this field is provided but ClientCapabilities.sampling.tools is not declared. - */ - tools?: Tool[]; - /** - * Controls how the model uses tools. - * The client MUST return an error if this field is provided but ClientCapabilities.sampling.tools is not declared. - * Default is `{ mode: "auto" }`. - */ - toolChoice?: ToolChoice; -} - -/** - * Controls tool selection behavior for sampling requests. - * - * @category `sampling/createMessage` - */ -export interface ToolChoice { - /** - * Controls the tool use ability of the model: - * - "auto": Model decides whether to use tools (default) - * - "required": Model MUST use at least one tool before completing - * - "none": Model MUST NOT use any tools - */ - mode?: 'auto' | 'required' | 'none'; -} - -/** - * A request from the server to sample an LLM via the client. The client has full discretion over which model to select. The client should also inform the user before beginning sampling, to allow them to inspect the request (human in the loop) and decide whether to approve it. - * - * @category `sampling/createMessage` - */ -export interface CreateMessageRequest extends JSONRPCRequest { - method: 'sampling/createMessage'; - params: CreateMessageRequestParams; -} - -/** - * The client's response to a sampling/createMessage request from the server. - * The client should inform the user before returning the sampled message, to allow them - * to inspect the response (human in the loop) and decide whether to allow the server to see it. - * - * @category `sampling/createMessage` - */ -export interface CreateMessageResult extends Result, SamplingMessage { - /** - * The name of the model that generated the message. - */ - model: string; - - /** - * The reason why sampling stopped, if known. - * - * Standard values: - * - "endTurn": Natural end of the assistant's turn - * - "stopSequence": A stop sequence was encountered - * - "maxTokens": Maximum token limit was reached - * - "toolUse": The model wants to use one or more tools - * - * This field is an open string to allow for provider-specific stop reasons. - */ - stopReason?: 'endTurn' | 'stopSequence' | 'maxTokens' | 'toolUse' | string; -} - -/** - * Describes a message issued to or received from an LLM API. - * - * @category `sampling/createMessage` - */ -export interface SamplingMessage { - role: Role; - content: SamplingMessageContentBlock | SamplingMessageContentBlock[]; - /** - * See [General fields: `_meta`](/specification/2025-11-25/basic/index#meta) for notes on `_meta` usage. - */ - _meta?: { [key: string]: unknown }; -} - -/** - * @category `sampling/createMessage` - */ -export type SamplingMessageContentBlock = TextContent | ImageContent | AudioContent | ToolUseContent | ToolResultContent; - -/** - * Optional annotations for the client. The client can use annotations to inform how objects are used or displayed - * - * @category Common Types - */ -export interface Annotations { - /** - * Describes who the intended audience of this object or data is. - * - * It can include multiple entries to indicate content useful for multiple audiences (e.g., `["user", "assistant"]`). - */ - audience?: Role[]; - - /** - * Describes how important this data is for operating the server. - * - * A value of 1 means "most important," and indicates that the data is - * effectively required, while 0 means "least important," and indicates that - * the data is entirely optional. - * - * @TJS-type number - * @minimum 0 - * @maximum 1 - */ - priority?: number; - - /** - * The moment the resource was last modified, as an ISO 8601 formatted string. - * - * Should be an ISO 8601 formatted string (e.g., "2025-01-12T15:00:58Z"). - * - * Examples: last activity timestamp in an open file, timestamp when the resource - * was attached, etc. - */ - lastModified?: string; -} - -/** - * @category Content - */ -export type ContentBlock = TextContent | ImageContent | AudioContent | ResourceLink | EmbeddedResource; - -/** - * Text provided to or from an LLM. - * - * @category Content - */ -export interface TextContent { - type: 'text'; - - /** - * The text content of the message. - */ - text: string; - - /** - * Optional annotations for the client. - */ - annotations?: Annotations; - - /** - * See [General fields: `_meta`](/specification/2025-11-25/basic/index#meta) for notes on `_meta` usage. - */ - _meta?: { [key: string]: unknown }; -} - -/** - * An image provided to or from an LLM. - * - * @category Content - */ -export interface ImageContent { - type: 'image'; - - /** - * The base64-encoded image data. - * - * @format byte - */ - data: string; - - /** - * The MIME type of the image. Different providers may support different image types. - */ - mimeType: string; - - /** - * Optional annotations for the client. - */ - annotations?: Annotations; - - /** - * See [General fields: `_meta`](/specification/2025-11-25/basic/index#meta) for notes on `_meta` usage. - */ - _meta?: { [key: string]: unknown }; -} - -/** - * Audio provided to or from an LLM. - * - * @category Content - */ -export interface AudioContent { - type: 'audio'; - - /** - * The base64-encoded audio data. - * - * @format byte - */ - data: string; - - /** - * The MIME type of the audio. Different providers may support different audio types. - */ - mimeType: string; - - /** - * Optional annotations for the client. - */ - annotations?: Annotations; - - /** - * See [General fields: `_meta`](/specification/2025-11-25/basic/index#meta) for notes on `_meta` usage. - */ - _meta?: { [key: string]: unknown }; -} - -/** - * A request from the assistant to call a tool. - * - * @category `sampling/createMessage` - */ -export interface ToolUseContent { - type: 'tool_use'; - - /** - * A unique identifier for this tool use. - * - * This ID is used to match tool results to their corresponding tool uses. - */ - id: string; - - /** - * The name of the tool to call. - */ - name: string; - - /** - * The arguments to pass to the tool, conforming to the tool's input schema. - */ - input: { [key: string]: unknown }; - - /** - * Optional metadata about the tool use. Clients SHOULD preserve this field when - * including tool uses in subsequent sampling requests to enable caching optimizations. - * - * See [General fields: `_meta`](/specification/2025-11-25/basic/index#meta) for notes on `_meta` usage. - */ - _meta?: { [key: string]: unknown }; -} - -/** - * The result of a tool use, provided by the user back to the assistant. - * - * @category `sampling/createMessage` - */ -export interface ToolResultContent { - type: 'tool_result'; - - /** - * The ID of the tool use this result corresponds to. - * - * This MUST match the ID from a previous ToolUseContent. - */ - toolUseId: string; - - /** - * The unstructured result content of the tool use. - * - * This has the same format as CallToolResult.content and can include text, images, - * audio, resource links, and embedded resources. - */ - content: ContentBlock[]; - - /** - * An optional structured result object. - * - * If the tool defined an outputSchema, this SHOULD conform to that schema. - */ - structuredContent?: { [key: string]: unknown }; - - /** - * Whether the tool use resulted in an error. - * - * If true, the content typically describes the error that occurred. - * Default: false - */ - isError?: boolean; - - /** - * Optional metadata about the tool result. Clients SHOULD preserve this field when - * including tool results in subsequent sampling requests to enable caching optimizations. - * - * See [General fields: `_meta`](/specification/2025-11-25/basic/index#meta) for notes on `_meta` usage. - */ - _meta?: { [key: string]: unknown }; -} - -/** - * The server's preferences for model selection, requested of the client during sampling. - * - * Because LLMs can vary along multiple dimensions, choosing the "best" model is - * rarely straightforward. Different models excel in different areas—some are - * faster but less capable, others are more capable but more expensive, and so - * on. This interface allows servers to express their priorities across multiple - * dimensions to help clients make an appropriate selection for their use case. - * - * These preferences are always advisory. The client MAY ignore them. It is also - * up to the client to decide how to interpret these preferences and how to - * balance them against other considerations. - * - * @category `sampling/createMessage` - */ -export interface ModelPreferences { - /** - * Optional hints to use for model selection. - * - * If multiple hints are specified, the client MUST evaluate them in order - * (such that the first match is taken). - * - * The client SHOULD prioritize these hints over the numeric priorities, but - * MAY still use the priorities to select from ambiguous matches. - */ - hints?: ModelHint[]; - - /** - * How much to prioritize cost when selecting a model. A value of 0 means cost - * is not important, while a value of 1 means cost is the most important - * factor. - * - * @TJS-type number - * @minimum 0 - * @maximum 1 - */ - costPriority?: number; - - /** - * How much to prioritize sampling speed (latency) when selecting a model. A - * value of 0 means speed is not important, while a value of 1 means speed is - * the most important factor. - * - * @TJS-type number - * @minimum 0 - * @maximum 1 - */ - speedPriority?: number; - - /** - * How much to prioritize intelligence and capabilities when selecting a - * model. A value of 0 means intelligence is not important, while a value of 1 - * means intelligence is the most important factor. - * - * @TJS-type number - * @minimum 0 - * @maximum 1 - */ - intelligencePriority?: number; -} - -/** - * Hints to use for model selection. - * - * Keys not declared here are currently left unspecified by the spec and are up - * to the client to interpret. - * - * @category `sampling/createMessage` - */ -export interface ModelHint { - /** - * A hint for a model name. - * - * The client SHOULD treat this as a substring of a model name; for example: - * - `claude-3-5-sonnet` should match `claude-3-5-sonnet-20241022` - * - `sonnet` should match `claude-3-5-sonnet-20241022`, `claude-3-sonnet-20240229`, etc. - * - `claude` should match any Claude model - * - * The client MAY also map the string to a different provider's model name or a different model family, as long as it fills a similar niche; for example: - * - `gemini-1.5-flash` could match `claude-3-haiku-20240307` - */ - name?: string; -} - -/* Autocomplete */ -/** - * Parameters for a `completion/complete` request. - * - * @category `completion/complete` - */ -export interface CompleteRequestParams extends RequestParams { - ref: PromptReference | ResourceTemplateReference; - /** - * The argument's information - */ - argument: { - /** - * The name of the argument - */ - name: string; - /** - * The value of the argument to use for completion matching. - */ - value: string; - }; - - /** - * Additional, optional context for completions - */ - context?: { - /** - * Previously-resolved variables in a URI template or prompt. - */ - arguments?: { [key: string]: string }; - }; -} - -/** - * A request from the client to the server, to ask for completion options. - * - * @category `completion/complete` - */ -export interface CompleteRequest extends JSONRPCRequest { - method: 'completion/complete'; - params: CompleteRequestParams; -} - -/** - * The server's response to a completion/complete request - * - * @category `completion/complete` - */ -export interface CompleteResult extends Result { - completion: { - /** - * An array of completion values. Must not exceed 100 items. - */ - values: string[]; - /** - * The total number of completion options available. This can exceed the number of values actually sent in the response. - */ - total?: number; - /** - * Indicates whether there are additional completion options beyond those provided in the current response, even if the exact total is unknown. - */ - hasMore?: boolean; - }; -} - -/** - * A reference to a resource or resource template definition. - * - * @category `completion/complete` - */ -export interface ResourceTemplateReference { - type: 'ref/resource'; - /** - * The URI or URI template of the resource. - * - * @format uri-template - */ - uri: string; -} - -/** - * Identifies a prompt. - * - * @category `completion/complete` - */ -export interface PromptReference extends BaseMetadata { - type: 'ref/prompt'; -} - -/* Roots */ -/** - * Sent from the server to request a list of root URIs from the client. Roots allow - * servers to ask for specific directories or files to operate on. A common example - * for roots is providing a set of repositories or directories a server should operate - * on. - * - * This request is typically used when the server needs to understand the file system - * structure or access specific locations that the client has permission to read from. - * - * @category `roots/list` - */ -export interface ListRootsRequest extends JSONRPCRequest { - method: 'roots/list'; - params?: RequestParams; -} - -/** - * The client's response to a roots/list request from the server. - * This result contains an array of Root objects, each representing a root directory - * or file that the server can operate on. - * - * @category `roots/list` - */ -export interface ListRootsResult extends Result { - roots: Root[]; -} - -/** - * Represents a root directory or file that the server can operate on. - * - * @category `roots/list` - */ -export interface Root { - /** - * The URI identifying the root. This *must* start with file:// for now. - * This restriction may be relaxed in future versions of the protocol to allow - * other URI schemes. - * - * @format uri - */ - uri: string; - /** - * An optional name for the root. This can be used to provide a human-readable - * identifier for the root, which may be useful for display purposes or for - * referencing the root in other parts of the application. - */ - name?: string; - - /** - * See [General fields: `_meta`](/specification/2025-11-25/basic/index#meta) for notes on `_meta` usage. - */ - _meta?: { [key: string]: unknown }; -} - -/** - * A notification from the client to the server, informing it that the list of roots has changed. - * This notification should be sent whenever the client adds, removes, or modifies any root. - * The server should then request an updated list of roots using the ListRootsRequest. - * - * @category `notifications/roots/list_changed` - */ -export interface RootsListChangedNotification extends JSONRPCNotification { - method: 'notifications/roots/list_changed'; - params?: NotificationParams; -} - -/** - * The parameters for a request to elicit non-sensitive information from the user via a form in the client. - * - * @category `elicitation/create` - */ -export interface ElicitRequestFormParams extends TaskAugmentedRequestParams { - /** - * The elicitation mode. - */ - mode?: 'form'; - - /** - * The message to present to the user describing what information is being requested. - */ - message: string; - - /** - * A restricted subset of JSON Schema. - * Only top-level properties are allowed, without nesting. - */ - requestedSchema: { - $schema?: string; - type: 'object'; - properties: { - [key: string]: PrimitiveSchemaDefinition; - }; - required?: string[]; - }; -} - -/** - * The parameters for a request to elicit information from the user via a URL in the client. - * - * @category `elicitation/create` - */ -export interface ElicitRequestURLParams extends TaskAugmentedRequestParams { - /** - * The elicitation mode. - */ - mode: 'url'; - - /** - * The message to present to the user explaining why the interaction is needed. - */ - message: string; - - /** - * The ID of the elicitation, which must be unique within the context of the server. - * The client MUST treat this ID as an opaque value. - */ - elicitationId: string; - - /** - * The URL that the user should navigate to. - * - * @format uri - */ - url: string; -} - -/** - * The parameters for a request to elicit additional information from the user via the client. - * - * @category `elicitation/create` - */ -export type ElicitRequestParams = ElicitRequestFormParams | ElicitRequestURLParams; - -/** - * A request from the server to elicit additional information from the user via the client. - * - * @category `elicitation/create` - */ -export interface ElicitRequest extends JSONRPCRequest { - method: 'elicitation/create'; - params: ElicitRequestParams; -} - -/** - * Restricted schema definitions that only allow primitive types - * without nested objects or arrays. - * - * @category `elicitation/create` - */ -export type PrimitiveSchemaDefinition = StringSchema | NumberSchema | BooleanSchema | EnumSchema; - -/** - * @category `elicitation/create` - */ -export interface StringSchema { - type: 'string'; - title?: string; - description?: string; - minLength?: number; - maxLength?: number; - format?: 'email' | 'uri' | 'date' | 'date-time'; - default?: string; -} - -/** - * @category `elicitation/create` - */ -export interface NumberSchema { - type: 'number' | 'integer'; - title?: string; - description?: string; - minimum?: number; - maximum?: number; - default?: number; -} - -/** - * @category `elicitation/create` - */ -export interface BooleanSchema { - type: 'boolean'; - title?: string; - description?: string; - default?: boolean; -} - -/** - * Schema for single-selection enumeration without display titles for options. - * - * @category `elicitation/create` - */ -export interface UntitledSingleSelectEnumSchema { - type: 'string'; - /** - * Optional title for the enum field. - */ - title?: string; - /** - * Optional description for the enum field. - */ - description?: string; - /** - * Array of enum values to choose from. - */ - enum: string[]; - /** - * Optional default value. - */ - default?: string; -} - -/** - * Schema for single-selection enumeration with display titles for each option. - * - * @category `elicitation/create` - */ -export interface TitledSingleSelectEnumSchema { - type: 'string'; - /** - * Optional title for the enum field. - */ - title?: string; - /** - * Optional description for the enum field. - */ - description?: string; - /** - * Array of enum options with values and display labels. - */ - oneOf: Array<{ - /** - * The enum value. - */ - const: string; - /** - * Display label for this option. - */ - title: string; - }>; - /** - * Optional default value. - */ - default?: string; -} - -/** - * @category `elicitation/create` - */ -// Combined single selection enumeration -export type SingleSelectEnumSchema = UntitledSingleSelectEnumSchema | TitledSingleSelectEnumSchema; - -/** - * Schema for multiple-selection enumeration without display titles for options. - * - * @category `elicitation/create` - */ -export interface UntitledMultiSelectEnumSchema { - type: 'array'; - /** - * Optional title for the enum field. - */ - title?: string; - /** - * Optional description for the enum field. - */ - description?: string; - /** - * Minimum number of items to select. - */ - minItems?: number; - /** - * Maximum number of items to select. - */ - maxItems?: number; - /** - * Schema for the array items. - */ - items: { - type: 'string'; - /** - * Array of enum values to choose from. - */ - enum: string[]; - }; - /** - * Optional default value. - */ - default?: string[]; -} - -/** - * Schema for multiple-selection enumeration with display titles for each option. - * - * @category `elicitation/create` - */ -export interface TitledMultiSelectEnumSchema { - type: 'array'; - /** - * Optional title for the enum field. - */ - title?: string; - /** - * Optional description for the enum field. - */ - description?: string; - /** - * Minimum number of items to select. - */ - minItems?: number; - /** - * Maximum number of items to select. - */ - maxItems?: number; - /** - * Schema for array items with enum options and display labels. - */ - items: { - /** - * Array of enum options with values and display labels. - */ - anyOf: Array<{ - /** - * The constant enum value. - */ - const: string; - /** - * Display title for this option. - */ - title: string; - }>; - }; - /** - * Optional default value. - */ - default?: string[]; -} - -/** - * @category `elicitation/create` - */ -// Combined multiple selection enumeration -export type MultiSelectEnumSchema = UntitledMultiSelectEnumSchema | TitledMultiSelectEnumSchema; - -/** - * Use TitledSingleSelectEnumSchema instead. - * This interface will be removed in a future version. - * - * @category `elicitation/create` - */ -export interface LegacyTitledEnumSchema { - type: 'string'; - title?: string; - description?: string; - enum: string[]; - /** - * (Legacy) Display names for enum values. - * Non-standard according to JSON schema 2020-12. - */ - enumNames?: string[]; - default?: string; -} - -/** - * @category `elicitation/create` - */ -// Union type for all enum schemas -export type EnumSchema = SingleSelectEnumSchema | MultiSelectEnumSchema | LegacyTitledEnumSchema; - -/** - * The client's response to an elicitation request. - * - * @category `elicitation/create` - */ -export interface ElicitResult extends Result { - /** - * The user action in response to the elicitation. - * - "accept": User submitted the form/confirmed the action - * - "decline": User explicitly decline the action - * - "cancel": User dismissed without making an explicit choice - */ - action: 'accept' | 'decline' | 'cancel'; - - /** - * The submitted form data, only present when action is "accept" and mode was "form". - * Contains values matching the requested schema. - * Omitted for out-of-band mode responses. - */ - content?: { [key: string]: string | number | boolean | string[] }; -} - -/** - * An optional notification from the server to the client, informing it of a completion of a out-of-band elicitation request. - * - * @category `notifications/elicitation/complete` - */ -export interface ElicitationCompleteNotification extends JSONRPCNotification { - method: 'notifications/elicitation/complete'; - params: { - /** - * The ID of the elicitation that completed. - */ - elicitationId: string; - }; -} - -/* Client messages */ -/** @internal */ -export type ClientRequest = - | PingRequest - | InitializeRequest - | CompleteRequest - | SetLevelRequest - | GetPromptRequest - | ListPromptsRequest - | ListResourcesRequest - | ListResourceTemplatesRequest - | ReadResourceRequest - | SubscribeRequest - | UnsubscribeRequest - | CallToolRequest - | ListToolsRequest - | GetTaskRequest - | GetTaskPayloadRequest - | ListTasksRequest - | CancelTaskRequest; - -/** @internal */ -export type ClientNotification = - | CancelledNotification - | ProgressNotification - | InitializedNotification - | RootsListChangedNotification - | TaskStatusNotification; - -/** @internal */ -export type ClientResult = - | EmptyResult - | CreateMessageResult - | ListRootsResult - | ElicitResult - | GetTaskResult - | GetTaskPayloadResult - | ListTasksResult - | CancelTaskResult; - -/* Server messages */ -/** @internal */ -export type ServerRequest = - | PingRequest - | CreateMessageRequest - | ListRootsRequest - | ElicitRequest - | GetTaskRequest - | GetTaskPayloadRequest - | ListTasksRequest - | CancelTaskRequest; - -/** @internal */ -export type ServerNotification = - | CancelledNotification - | ProgressNotification - | LoggingMessageNotification - | ResourceUpdatedNotification - | ResourceListChangedNotification - | ToolListChangedNotification - | PromptListChangedNotification - | ElicitationCompleteNotification - | TaskStatusNotification; - -/** @internal */ -export type ServerResult = - | EmptyResult - | InitializeResult - | CompleteResult - | GetPromptResult - | ListPromptsResult - | ListResourceTemplatesResult - | ListResourcesResult - | ReadResourceResult - | CallToolResult - | ListToolsResult - | GetTaskResult - | GetTaskPayloadResult - | ListTasksResult - | CancelTaskResult; diff --git a/packages/core/src/types/spec.types.2026-07-28.ts b/packages/core/src/types/spec.types.2026-07-28.ts deleted file mode 100644 index 7305df0462..0000000000 --- a/packages/core/src/types/spec.types.2026-07-28.ts +++ /dev/null @@ -1,3030 +0,0 @@ -/** - * This file is automatically generated from the Model Context Protocol specification. - * - * Source: https://github.com/modelcontextprotocol/modelcontextprotocol - * Pulled from: https://raw.githubusercontent.com/modelcontextprotocol/modelcontextprotocol/main/schema/draft/schema.ts - * Last updated from commit: 9d700ed62dcf86cb77475c9b81930611a9182f46 - * - * DO NOT EDIT THIS FILE MANUALLY. Changes will be overwritten by automated updates. - * To update this file, run: pnpm run fetch:spec-types 2026-07-28 - */ /* JSON types */ - -/** - * @category Common Types - */ -export type JSONValue = string | number | boolean | null | JSONObject | JSONArray; - -/** - * @category Common Types - */ -export type JSONObject = { [key: string]: JSONValue }; - -/** - * @category Common Types - */ -export type JSONArray = JSONValue[]; - -/* JSON-RPC types */ - -/** - * Refers to any valid JSON-RPC object that can be decoded off the wire, or encoded to be sent. - * - * @category JSON-RPC - */ -export type JSONRPCMessage = JSONRPCRequest | JSONRPCNotification | JSONRPCResponse; - -/** @internal */ -export const LATEST_PROTOCOL_VERSION = '2026-07-28'; -/** @internal */ -export const JSONRPC_VERSION = '2.0'; - -/** - * Represents the contents of a `_meta` field, which clients and servers use to attach additional metadata to their interactions. - * - * Certain key names are reserved by MCP for protocol-level metadata; implementations MUST NOT make assumptions about values at these keys. Additionally, specific schema definitions may reserve particular names for purpose-specific metadata, as declared in those definitions. - * - * Valid keys have two segments: - * - * **Prefix:** - * - Optional — if specified, MUST be a series of _labels_ separated by dots (`.`), followed by a slash (`/`). - * - Labels MUST start with a letter and end with a letter or digit. Interior characters may be letters, digits, or hyphens (`-`). - * - Implementations SHOULD use reverse DNS notation (e.g., `com.example/` rather than `example.com/`). - * - Any prefix where the second label is `modelcontextprotocol` or `mcp` is **reserved** for MCP use. For example: `io.modelcontextprotocol/`, `dev.mcp/`, `org.modelcontextprotocol.api/`, and `com.mcp.tools/` are all reserved. However, `com.example.mcp/` is NOT reserved, as the second label is `example`. - * - * **Name:** - * - Unless empty, MUST start and end with an alphanumeric character (`[a-z0-9A-Z]`). - * - Interior characters may be alphanumeric, hyphens (`-`), underscores (`_`), or dots (`.`). - * - * @see [General fields: `_meta`](/specification/draft/basic/index#meta) for more details. - * @category Common Types - */ -export type MetaObject = Record; - -/** - * Extends {@link MetaObject} with additional request-specific fields. All key naming rules from `MetaObject` apply. - * - * @see {@link MetaObject} for key naming rules and reserved prefixes. - * @see [General fields: `_meta`](/specification/draft/basic/index#meta) for more details. - * @category Common Types - */ -export interface RequestMetaObject extends MetaObject { - /** - * If specified, the caller is requesting out-of-band progress notifications for this request (as represented by {@link ProgressNotification | notifications/progress}). The value of this parameter is an opaque token that will be attached to any subsequent notifications. The receiver is not obligated to provide these notifications. - */ - progressToken?: ProgressToken; - /** - * The MCP Protocol Version being used for this request. Required. - * - * For the HTTP transport, this value MUST match the `MCP-Protocol-Version` - * header; otherwise the server MUST return a `400 Bad Request`. If the - * server does not support the requested version, it MUST return an - * {@link UnsupportedProtocolVersionError}. - */ - 'io.modelcontextprotocol/protocolVersion': string; - /** - * Identifies the client software making the request. Required. - * - * The {@link Implementation} schema requires `name` and `version`; other - * fields are optional. - */ - 'io.modelcontextprotocol/clientInfo': Implementation; - /** - * The client's capabilities for this specific request. Required. - * - * Capabilities are declared per-request rather than once at initialization; - * an empty object means the client supports no optional capabilities. - * Servers MUST NOT infer capabilities from prior requests. - */ - 'io.modelcontextprotocol/clientCapabilities': ClientCapabilities; - /** - * The desired log level for this request. Optional. - * - * If absent, the server MUST NOT send any {@link LoggingMessageNotification | notifications/message} - * notifications for this request. The client opts in to log messages by - * explicitly setting a level. Replaces the former `logging/setLevel` RPC. - * - * @deprecated Deprecated as of protocol version 2026-07-28 (SEP-2577). - * Remains in the specification for at least twelve months; see the - * deprecated features registry. - */ - 'io.modelcontextprotocol/logLevel'?: LoggingLevel; -} - -/** - * A progress token, used to associate progress notifications with the original request. - * - * @category Common Types - */ -export type ProgressToken = string | number; - -/** - * An opaque token used to represent a cursor for pagination. - * - * @category Common Types - */ -export type Cursor = string; - -/** - * Common params for any request. - * - * @category Common Types - */ -export interface RequestParams { - _meta: RequestMetaObject; -} - -/** @internal */ -export interface Request { - method: string; - // Allow unofficial extensions of `Request.params` without impacting `RequestParams`. - // eslint-disable-next-line @typescript-eslint/no-explicit-any - params?: { [key: string]: any }; -} - -/** - * Common params for any notification. - * - * @category Common Types - */ -export interface NotificationParams { - _meta?: MetaObject; -} - -/** @internal */ -export interface Notification { - method: string; - // Allow unofficial extensions of `Notification.params` without impacting `NotificationParams`. - // eslint-disable-next-line @typescript-eslint/no-explicit-any - params?: { [key: string]: any }; -} - -/** - * Indicates the type of a {@link Result} object, allowing the client to - * determine how to parse the response. - * - * complete - the request completed successfully and the result contains the final content. - * input_required - the request requires additional input and the result contains an {@link InputRequiredResult} object with instructions for the client to provide additional input before retrying the original request. - * @category Common Types - */ -export type ResultType = 'complete' | 'input_required' | string; - -/** - * Common result fields. - * - * @category Common Types - */ -export interface Result { - _meta?: MetaObject; - /** - * Indicates the type of the result, which allows the client to determine - * how to parse the result object. - * - * Servers implementing this protocol version MUST include this field. - * For backward compatibility, when a client receives a result from a - * server implementing an earlier protocol version (which does not include - * `resultType`), the client MUST treat the absent field as `"complete"`. - */ - resultType: ResultType; - [key: string]: unknown; -} - -/** - * @category Errors - */ -export interface Error { - /** - * The error type that occurred. - */ - code: number; - /** - * A short description of the error. The message SHOULD be limited to a concise single sentence. - */ - message: string; - /** - * Additional information about the error. The value of this member is defined by the sender (e.g. detailed error information, nested errors etc.). - */ - data?: unknown; -} - -/** - * A uniquely identifying ID for a request in JSON-RPC. - * - * @category Common Types - */ -export type RequestId = string | number; - -/** - * A request that expects a response. - * - * @category JSON-RPC - */ -export interface JSONRPCRequest extends Request { - jsonrpc: typeof JSONRPC_VERSION; - id: RequestId; -} - -/** - * A notification which does not expect a response. - * - * @category JSON-RPC - */ -export interface JSONRPCNotification extends Notification { - jsonrpc: typeof JSONRPC_VERSION; -} - -/** - * A successful (non-error) response to a request. - * - * @category JSON-RPC - */ -export interface JSONRPCResultResponse { - jsonrpc: typeof JSONRPC_VERSION; - id: RequestId; - result: Result; -} - -/** - * A response to a request that indicates an error occurred. - * - * @category JSON-RPC - */ -export interface JSONRPCErrorResponse { - jsonrpc: typeof JSONRPC_VERSION; - id?: RequestId; - error: Error; -} - -/** - * A response to a request, containing either the result or error. - * - * @category JSON-RPC - */ -export type JSONRPCResponse = JSONRPCResultResponse | JSONRPCErrorResponse; - -// Standard JSON-RPC error codes -export const PARSE_ERROR = -32700; -export const INVALID_REQUEST = -32600; -export const METHOD_NOT_FOUND = -32601; -export const INVALID_PARAMS = -32602; -export const INTERNAL_ERROR = -32603; - -/** - * A JSON-RPC error indicating that invalid JSON was received by the server. This error is returned when the server cannot parse the JSON text of a message. - * - * @see {@link https://www.jsonrpc.org/specification#error_object | JSON-RPC 2.0 Error Object} - * - * @example Invalid JSON - * {@includeCode ./examples/ParseError/invalid-json.json} - * - * @category Errors - */ -export interface ParseError extends Error { - code: typeof PARSE_ERROR; -} - -/** - * A JSON-RPC error indicating that the request is not a valid request object. This error is returned when the message structure does not conform to the JSON-RPC 2.0 specification requirements for a request (e.g., missing required fields like `jsonrpc` or `method`, or using invalid types for these fields). - * - * @see {@link https://www.jsonrpc.org/specification#error_object | JSON-RPC 2.0 Error Object} - * - * @category Errors - */ -export interface InvalidRequestError extends Error { - code: typeof INVALID_REQUEST; -} - -/** - * A JSON-RPC error indicating that the requested method does not exist or is not available. - * - * In MCP, a server returns this error when a client invokes a method the server does not implement — either a genuinely unknown method, or one gated behind a server capability the server did not advertise (e.g., calling `prompts/list` when the `prompts` capability was not advertised). - * - * A request that requires a client capability the client did not declare is signalled instead by {@link MissingRequiredClientCapabilityError} (`-32003`). - * - * @see {@link https://www.jsonrpc.org/specification#error_object | JSON-RPC 2.0 Error Object} - * - * @example Prompts not supported - * {@includeCode ./examples/MethodNotFoundError/prompts-not-supported.json} - * - * @category Errors - */ -export interface MethodNotFoundError extends Error { - code: typeof METHOD_NOT_FOUND; -} - -/** - * A JSON-RPC error indicating that the method parameters are invalid or malformed. - * - * In MCP, this error is returned in various contexts when request parameters fail validation: - * - * - **Tools**: Unknown tool name or invalid tool arguments - * - **Prompts**: Unknown prompt name or missing required arguments - * - **Pagination**: Invalid or expired cursor values - * - **Logging**: Invalid log level - * - **Elicitation**: Server requests an elicitation mode not declared in client capabilities - * - **Sampling**: Missing tool result or tool results mixed with other content - * - * @see {@link https://www.jsonrpc.org/specification#error_object | JSON-RPC 2.0 Error Object} - * - * @example Unknown tool - * {@includeCode ./examples/InvalidParamsError/unknown-tool.json} - * - * @example Invalid tool arguments - * {@includeCode ./examples/InvalidParamsError/invalid-tool-arguments.json} - * - * @example Unknown prompt - * {@includeCode ./examples/InvalidParamsError/unknown-prompt.json} - * - * @example Invalid cursor - * {@includeCode ./examples/InvalidParamsError/invalid-cursor.json} - * - * @category Errors - */ -export interface InvalidParamsError extends Error { - code: typeof INVALID_PARAMS; -} - -/** - * A JSON-RPC error indicating that an internal error occurred on the receiver. This error is returned when the receiver encounters an unexpected condition that prevents it from fulfilling the request. - * - * @see {@link https://www.jsonrpc.org/specification#error_object | JSON-RPC 2.0 Error Object} - * - * @example Unexpected error - * {@includeCode ./examples/InternalError/unexpected-error.json} - * - * @category Errors - */ -export interface InternalError extends Error { - code: typeof INTERNAL_ERROR; -} - -/** - * Error code returned when a server requires a client capability that was - * not declared in the request's `clientCapabilities`. - * - * @category Errors - */ -export const MISSING_REQUIRED_CLIENT_CAPABILITY = -32003; - -/** - * Error code returned when the request's protocol version is not supported - * by the server. - * - * @category Errors - */ -export const UNSUPPORTED_PROTOCOL_VERSION = -32004; - -/** - * Returned when the request's protocol version is unknown to the server or - * unsupported (e.g., a known experimental or draft version the server has - * chosen not to implement). For HTTP, the response status code MUST be - * `400 Bad Request`. - * - * @example Unsupported protocol version - * {@includeCode ./examples/UnsupportedProtocolVersionError/unsupported-version.json} - * - * @category Errors - */ -export interface UnsupportedProtocolVersionError extends Omit { - error: Error & { - code: typeof UNSUPPORTED_PROTOCOL_VERSION; - data: { - /** - * Protocol versions the server supports. The client should choose a - * mutually supported version from this list and retry. - */ - supported: string[]; - /** - * The protocol version that was requested by the client. - */ - requested: string; - }; - }; -} - -/** - * Returned when processing a request requires a capability the client did not - * declare in `clientCapabilities`. For HTTP, the response status code MUST be - * `400 Bad Request`. - * - * @example Missing elicitation capability - * {@includeCode ./examples/MissingRequiredClientCapabilityError/missing-elicitation-capability.json} - * - * @category Errors - */ -export interface MissingRequiredClientCapabilityError extends Omit { - error: Error & { - code: typeof MISSING_REQUIRED_CLIENT_CAPABILITY; - data: { - /** - * The capabilities the server requires from the client to process this request. - */ - requiredCapabilities: ClientCapabilities; - }; - }; -} - -/* Empty result */ -/** - * A result that indicates success but carries no data. - * - * @category Common Types - */ -export type EmptyResult = Result; - -/** @internal */ -export type InputRequest = CreateMessageRequest | ListRootsRequest | ElicitRequest; - -/** @internal */ -export type InputResponse = CreateMessageResult | ListRootsResult | ElicitResult; - -/** - * A map of server-initiated requests that the client must fulfill. - * Keys are server-assigned identifiers; values are the request objects. - * - * @example Elicitation and sampling input requests - * {@includeCode ./examples/InputRequests/elicitation-and-sampling-input-requests.json} - * - * @category Multi Round-Trip - */ -export interface InputRequests { - [key: string]: InputRequest; -} - -/** - * A map of client responses to server-initiated requests. - * Keys correspond to the keys in the {@link InputRequests} map; - * values are the client's result for each request. - * - * @example Elicitation and sampling input responses - * {@includeCode ./examples/InputResponses/elicitation-and-sampling-input-responses.json} - * - * @category Multi Round-Trip - */ -export interface InputResponses { - [key: string]: InputResponse; -} - -/** - * An InputRequiredResult sent by the server to indicate that additional input is needed - * before the request can be completed. - * - * At least one of `inputRequests` or `requestState` MUST be present. - * @example InputRequiredResult with elicitation and sampling input requests and request state - * {@includeCode ./examples/InputRequiredResult/input-required-result-with-elicitation-and-sampling-and-request-state.json} - * - * @example InputRequiredResult with request state only (load shedding) - * {@includeCode ./examples/InputRequiredResult/input-required-result-with-request-state-only.json} - * - * @category Multi Round-Trip - */ -export interface InputRequiredResult extends Result { - /* Requests issued by the server that must be complete before the - * client can retry the original request. - */ - inputRequests?: InputRequests; - /* Request state to be passed back to the server when the client - * retries the original request. - * Note: The client must treat this as an opaque blob; it must not - * interpret it in any way. - */ - requestState?: string; -} - -/* Request parameter type that includes input responses and request state. - * These parameters may be included in any client-initiated request. - */ -export interface InputResponseRequestParams extends RequestParams { - /* New field to carry the responses for the server's requests from the - * InputRequiredResult message. For each key in the response's inputRequests - * field, the same key must appear here with the associated response. - */ - inputResponses?: InputResponses; - /* Request state passed back to the server from the client. - */ - requestState?: string; -} - -/* Cancellation */ -/** - * Parameters for a `notifications/cancelled` notification. - * - * @example User-requested cancellation - * {@includeCode ./examples/CancelledNotificationParams/user-requested-cancellation.json} - * - * @category `notifications/cancelled` - */ -export interface CancelledNotificationParams extends NotificationParams { - /** - * The ID of the request to cancel. - * - * This MUST correspond to the ID of a request previously issued in the same direction. - */ - requestId?: RequestId; - - /** - * An optional string describing the reason for the cancellation. This MAY be logged or presented to the user. - */ - reason?: string; -} - -/** - * This notification can be sent by either side to indicate that it is cancelling a previously-issued request. - * - * The request SHOULD still be in-flight, but due to communication latency, it is always possible that this notification MAY arrive after the request has already finished. - * - * This notification indicates that the result will be unused, so any associated processing SHOULD cease. - * - * @example User-requested cancellation - * {@includeCode ./examples/CancelledNotification/user-requested-cancellation.json} - * - * @category `notifications/cancelled` - */ -export interface CancelledNotification extends JSONRPCNotification { - method: 'notifications/cancelled'; - params: CancelledNotificationParams; -} - -/* Discovery */ -/** - * A request from the client asking the server to advertise its supported - * protocol versions, capabilities, and other metadata. Servers **MUST** - * implement `server/discover`. Clients **MAY** call it but are not required - * to — version negotiation can also happen inline via per-request `_meta`. - * - * @example Discover request - * {@includeCode ./examples/DiscoverRequest/server-discover-request.json} - * - * @category `server/discover` - */ -export interface DiscoverRequest extends JSONRPCRequest { - method: 'server/discover'; - params: RequestParams; -} - -/** - * The result returned by the server for a {@link DiscoverRequest | server/discover} request. - * - * @example Server capabilities discovery - * {@includeCode ./examples/DiscoverResult/server-capabilities-discovery.json} - * - * @category `server/discover` - */ -export interface DiscoverResult extends Result { - /** - * MCP Protocol Versions this server supports. The client should choose a - * version from this list for use in subsequent requests. - */ - supportedVersions: string[]; - /** - * The capabilities of the server. - */ - capabilities: ServerCapabilities; - /** - * Information about the server software implementation. - */ - serverInfo: Implementation; - /** - * Natural-language guidance describing the server and its features. - * - * This can be used by clients to improve an LLM's understanding of - * available tools (e.g., by including it in a system prompt). It should - * focus on information that helps the model use the server effectively - * and should not duplicate information already in tool descriptions. - */ - instructions?: string; -} - -/** - * A successful response from the server for a {@link DiscoverRequest | server/discover} request. - * - * @example Discover result response - * {@includeCode ./examples/DiscoverResultResponse/discover-result-response.json} - * - * @category `server/discover` - */ -export interface DiscoverResultResponse extends JSONRPCResultResponse { - result: DiscoverResult; -} - -/** - * Capabilities a client may support. Known capabilities are defined here, in this schema, but this is not a closed set: any client can define its own, additional capabilities. - * - * @category `server/discover` - */ -export interface ClientCapabilities { - /** - * Experimental, non-standard capabilities that the client supports. - */ - experimental?: { [key: string]: JSONObject }; - /** - * Present if the client supports listing roots. - * - * @deprecated Deprecated as of protocol version 2026-07-28 (SEP-2577). - * Remains in the specification for at least twelve months; see the - * deprecated features registry. - * - * @example Roots — minimum baseline support - * {@includeCode ./examples/ClientCapabilities/roots-minimum-baseline-support.json} - */ - // eslint-disable-next-line @typescript-eslint/no-empty-object-type - roots?: {}; - /** - * Present if the client supports sampling from an LLM. - * - * @deprecated Deprecated as of protocol version 2026-07-28 (SEP-2577). - * Remains in the specification for at least twelve months; see the - * deprecated features registry. - * - * @example Sampling — minimum baseline support - * {@includeCode ./examples/ClientCapabilities/sampling-minimum-baseline-support.json} - * - * @example Sampling — tool use support - * {@includeCode ./examples/ClientCapabilities/sampling-tool-use-support.json} - * - * @example Sampling — context inclusion support (deprecated) - * {@includeCode ./examples/ClientCapabilities/sampling-context-inclusion-support-deprecated.json} - */ - sampling?: { - /** - * Whether the client supports context inclusion via `includeContext` parameter. - * If not declared, servers SHOULD only use `includeContext: "none"` (or omit it). - */ - context?: JSONObject; - /** - * Whether the client supports tool use via `tools` and `toolChoice` parameters. - */ - tools?: JSONObject; - }; - /** - * Present if the client supports elicitation from the server. - * - * @example Elicitation — form and URL mode support - * {@includeCode ./examples/ClientCapabilities/elicitation-form-and-url-mode-support.json} - * - * @example Elicitation — form mode only (implicit) - * {@includeCode ./examples/ClientCapabilities/elicitation-form-only-implicit.json} - */ - elicitation?: { - form?: JSONObject; - url?: JSONObject; - }; - - /** - * Optional MCP extensions that the client supports. Keys are extension identifiers - * (e.g., "io.modelcontextprotocol/oauth-client-credentials"), and values are - * per-extension settings objects. An empty object indicates support with no settings. - * - * @example Extensions — MCP Apps (UI) extension with MIME type support - * {@includeCode ./examples/ClientCapabilities/extensions-ui-mime-types.json} - */ - extensions?: { [key: string]: JSONObject }; -} - -/** - * Capabilities that a server may support. Known capabilities are defined here, in this schema, but this is not a closed set: any server can define its own, additional capabilities. - * - * @category `server/discover` - */ -export interface ServerCapabilities { - /** - * Experimental, non-standard capabilities that the server supports. - */ - experimental?: { [key: string]: JSONObject }; - /** - * Present if the server supports sending log messages to the client. - * - * @deprecated Deprecated as of protocol version 2026-07-28 (SEP-2577). - * Remains in the specification for at least twelve months; see the - * deprecated features registry. - * - * @example Logging — minimum baseline support - * {@includeCode ./examples/ServerCapabilities/logging-minimum-baseline-support.json} - */ - logging?: JSONObject; - /** - * Present if the server supports argument autocompletion suggestions. - * - * @example Completions — minimum baseline support - * {@includeCode ./examples/ServerCapabilities/completions-minimum-baseline-support.json} - */ - completions?: JSONObject; - /** - * Present if the server offers any prompt templates. - * - * @example Prompts — minimum baseline support - * {@includeCode ./examples/ServerCapabilities/prompts-minimum-baseline-support.json} - * - * @example Prompts — list changed notifications - * {@includeCode ./examples/ServerCapabilities/prompts-list-changed-notifications.json} - */ - prompts?: { - /** - * Whether this server supports notifications for changes to the prompt list. - */ - listChanged?: boolean; - }; - /** - * Present if the server offers any resources to read. - * - * @example Resources — minimum baseline support - * {@includeCode ./examples/ServerCapabilities/resources-minimum-baseline-support.json} - * - * @example Resources — subscription to individual resource updates (only) - * {@includeCode ./examples/ServerCapabilities/resources-subscription-to-individual-resource-updates-only.json} - * - * @example Resources — list changed notifications (only) - * {@includeCode ./examples/ServerCapabilities/resources-list-changed-notifications-only.json} - * - * @example Resources — all notifications - * {@includeCode ./examples/ServerCapabilities/resources-all-notifications.json} - */ - resources?: { - /** - * Whether this server supports subscribing to resource updates. - */ - subscribe?: boolean; - /** - * Whether this server supports notifications for changes to the resource list. - */ - listChanged?: boolean; - }; - /** - * Present if the server offers any tools to call. - * - * @example Tools — minimum baseline support - * {@includeCode ./examples/ServerCapabilities/tools-minimum-baseline-support.json} - * - * @example Tools — list changed notifications - * {@includeCode ./examples/ServerCapabilities/tools-list-changed-notifications.json} - */ - tools?: { - /** - * Whether this server supports notifications for changes to the tool list. - */ - listChanged?: boolean; - }; - /** - * Optional MCP extensions that the server supports. Keys are extension identifiers - * (e.g., "io.modelcontextprotocol/tasks"), and values are per-extension settings - * objects. An empty object indicates support with no settings. - * - * @example Extensions — Tasks extension support - * {@includeCode ./examples/ServerCapabilities/extensions-tasks.json} - */ - extensions?: { [key: string]: JSONObject }; -} - -/** - * An optionally-sized icon that can be displayed in a user interface. - * - * @category Common Types - */ -export interface Icon { - /** - * A standard URI pointing to an icon resource. May be an HTTP/HTTPS URL or a - * `data:` URI with Base64-encoded image data. - * - * Consumers SHOULD take steps to ensure URLs serving icons are from the - * same domain as the client/server or a trusted domain. - * - * Consumers SHOULD take appropriate precautions when consuming SVGs as they can contain - * executable JavaScript. - * - * @format uri - */ - src: string; - - /** - * Optional MIME type override if the source MIME type is missing or generic. - * For example: `"image/png"`, `"image/jpeg"`, or `"image/svg+xml"`. - */ - mimeType?: string; - - /** - * Optional array of strings that specify sizes at which the icon can be used. - * Each string should be in WxH format (e.g., `"48x48"`, `"96x96"`) or `"any"` for scalable formats like SVG. - * - * If not provided, the client should assume that the icon can be used at any size. - */ - sizes?: string[]; - - /** - * Optional specifier for the theme this icon is designed for. `"light"` indicates - * the icon is designed to be used with a light background, and `"dark"` indicates - * the icon is designed to be used with a dark background. - * - * If not provided, the client should assume the icon can be used with any theme. - */ - theme?: 'light' | 'dark'; -} - -/** - * Base interface to add `icons` property. - * - * @internal - */ -export interface Icons { - /** - * Optional set of sized icons that the client can display in a user interface. - * - * Clients that support rendering icons MUST support at least the following MIME types: - * - `image/png` - PNG images (safe, universal compatibility) - * - `image/jpeg` (and `image/jpg`) - JPEG images (safe, universal compatibility) - * - * Clients that support rendering icons SHOULD also support: - * - `image/svg+xml` - SVG images (scalable but requires security precautions) - * - `image/webp` - WebP images (modern, efficient format) - */ - icons?: Icon[]; -} - -/** - * Base interface for metadata with name (identifier) and title (display name) properties. - * - * @internal - */ -export interface BaseMetadata { - /** - * Intended for programmatic or logical use, but used as a display name in past specs or fallback (if title isn't present). - */ - name: string; - - /** - * Intended for UI and end-user contexts — optimized to be human-readable and easily understood, - * even by those unfamiliar with domain-specific terminology. - * - * If not provided, the name should be used for display (except for {@link Tool}, - * where `annotations.title` should be given precedence over using `name`, - * if present). - */ - title?: string; -} - -/** - * Describes the MCP implementation. - * - * @category `server/discover` - */ -export interface Implementation extends BaseMetadata, Icons { - /** - * The version of this implementation. - */ - version: string; - - /** - * An optional human-readable description of what this implementation does. - * - * This can be used by clients or servers to provide context about their purpose - * and capabilities. For example, a server might describe the types of resources - * or tools it provides, while a client might describe its intended use case. - */ - description?: string; - - /** - * An optional URL of the website for this implementation. - * - * @format uri - */ - websiteUrl?: string; -} - -/* Progress notifications */ - -/** - * Parameters for a {@link ProgressNotification | notifications/progress} notification. - * - * @example Progress message - * {@includeCode ./examples/ProgressNotificationParams/progress-message.json} - * - * @category `notifications/progress` - */ -export interface ProgressNotificationParams extends NotificationParams { - /** - * The progress token which was given in the initial request, used to associate this notification with the request that is proceeding. - */ - progressToken: ProgressToken; - /** - * The progress thus far. This should increase every time progress is made, even if the total is unknown. - * - * @TJS-type number - */ - progress: number; - /** - * Total number of items to process (or total progress required), if known. - * - * @TJS-type number - */ - total?: number; - /** - * An optional message describing the current progress. - */ - message?: string; -} - -/** - * An out-of-band notification used to inform the receiver of a progress update for a long-running request. - * - * @example Progress message - * {@includeCode ./examples/ProgressNotification/progress-message.json} - * - * @category `notifications/progress` - */ -export interface ProgressNotification extends JSONRPCNotification { - method: 'notifications/progress'; - params: ProgressNotificationParams; -} - -/* Pagination */ -/** - * Common params for paginated requests. - * - * @example List request with cursor - * {@includeCode ./examples/PaginatedRequestParams/list-with-cursor.json} - * - * @category Common Types - */ -export interface PaginatedRequestParams extends RequestParams { - /** - * An opaque token representing the current pagination position. - * If provided, the server should return results starting after this cursor. - */ - cursor?: Cursor; -} - -/** @internal */ -export interface PaginatedRequest extends JSONRPCRequest { - params: PaginatedRequestParams; -} - -/** @internal */ -export interface PaginatedResult extends Result { - /** - * An opaque token representing the pagination position after the last returned result. - * If present, there may be more results available. - */ - nextCursor?: Cursor; -} - -/** - * A result that supports a time-to-live (TTL) hint for client-side caching. - * - * @internal - */ -export interface CacheableResult extends Result { - /** - * A hint from the server indicating how long (in milliseconds) the - * client MAY cache this response before re-fetching. Semantics are - * analogous to HTTP Cache-Control max-age. - * - * - If 0, The response SHOULD be considered immediately stale, - * The client MAY re-fetch every time the result is needed. - * - If positive, the client SHOULD consider the result fresh for this many - * milliseconds after receiving the response. - * - * @minimum 0 - */ - ttlMs: number; - - /** - * Indicates the intended scope of the cached response, analogous to HTTP - * `Cache-Control: public` vs `Cache-Control: private`. - * - * - `"public"`: Any client or intermediary (e.g., shared gateway, proxy) - * MAY cache the response and serve it to any user. - * - `"private"`: Only the requesting user's client MAY cache the response. - * Shared caches (e.g., multi-tenant gateways) MUST NOT serve a cached - * copy to a different user. - * - */ - cacheScope: 'public' | 'private'; -} - -/* Resources */ -/** - * Sent from the client to request a list of resources the server has. - * - * @example List resources request - * {@includeCode ./examples/ListResourcesRequest/list-resources-request.json} - * - * @category `resources/list` - */ -export interface ListResourcesRequest extends PaginatedRequest { - method: 'resources/list'; -} - -/** - * The result returned by the server for a {@link ListResourcesRequest | resources/list} request. - * - * @example Resources list with cursor and TTL - * {@includeCode ./examples/ListResourcesResult/resources-list-with-cursor-and-ttl.json} - * - * @category `resources/list` - */ -export interface ListResourcesResult extends PaginatedResult, CacheableResult { - resources: Resource[]; -} - -/** - * A successful response from the server for a {@link ListResourcesRequest | resources/list} request. - * - * @example List resources result response - * {@includeCode ./examples/ListResourcesResultResponse/list-resources-result-response.json} - * - * @category `resources/list` - */ -export interface ListResourcesResultResponse extends JSONRPCResultResponse { - result: ListResourcesResult; -} - -/** - * Sent from the client to request a list of resource templates the server has. - * - * @example List resource templates request - * {@includeCode ./examples/ListResourceTemplatesRequest/list-resource-templates-request.json} - * - * @category `resources/templates/list` - */ -export interface ListResourceTemplatesRequest extends PaginatedRequest { - method: 'resources/templates/list'; -} - -/** - * The result returned by the server for a {@link ListResourceTemplatesRequest | resources/templates/list} request. - * - * @example Resource templates list with cursor and TTL - * {@includeCode ./examples/ListResourceTemplatesResult/resource-templates-list-with-cursor-and-ttl.json} - * - * @category `resources/templates/list` - */ -export interface ListResourceTemplatesResult extends PaginatedResult, CacheableResult { - resourceTemplates: ResourceTemplate[]; -} - -/** - * A successful response from the server for a {@link ListResourceTemplatesRequest | resources/templates/list} request. - * - * @example List resource templates result response - * {@includeCode ./examples/ListResourceTemplatesResultResponse/list-resource-templates-result-response.json} - * - * @category `resources/templates/list` - */ -export interface ListResourceTemplatesResultResponse extends JSONRPCResultResponse { - result: ListResourceTemplatesResult; -} - -/** - * Common params for resource-related requests. - * - * @internal - */ -export interface ResourceRequestParams extends RequestParams { - /** - * The URI of the resource. The URI can use any protocol; it is up to the server how to interpret it. - * - * @format uri - */ - uri: string; -} - -/** - * Parameters for a `resources/read` request. - * - * @category `resources/read` - */ -export interface ReadResourceRequestParams extends ResourceRequestParams, InputResponseRequestParams {} - -/** - * Sent from the client to the server, to read a specific resource URI. - * - * @example Read resource request - * {@includeCode ./examples/ReadResourceRequest/read-resource-request.json} - * - * @category `resources/read` - */ -export interface ReadResourceRequest extends JSONRPCRequest { - method: 'resources/read'; - params: ReadResourceRequestParams; -} - -/** - * The result returned by the server for a {@link ReadResourceRequest | resources/read} request. - * - * @example File resource contents - * {@includeCode ./examples/ReadResourceResult/file-resource-contents.json} - * - * @category `resources/read` - */ -export interface ReadResourceResult extends CacheableResult { - contents: (TextResourceContents | BlobResourceContents)[]; -} - -/** - * A successful response from the server for a {@link ReadResourceRequest | resources/read} request. - * - * @example Read resource result response - * {@includeCode ./examples/ReadResourceResultResponse/read-resource-result-response.json} - * - * @example Read resource result response with TTL - * {@includeCode ./examples/ReadResourceResultResponse/read-resource-result-response-with-ttl.json} - * - * @category `resources/read` - */ -export interface ReadResourceResultResponse extends JSONRPCResultResponse { - result: ReadResourceResult | InputRequiredResult; -} - -/** - * An optional notification from the server to the client, informing it that the list of resources it can read from has changed. This may be issued by servers without any previous subscription from the client. - * - * @example Resources list changed - * {@includeCode ./examples/ResourceListChangedNotification/resources-list-changed.json} - * - * @category `notifications/resources/list_changed` - */ -export interface ResourceListChangedNotification extends JSONRPCNotification { - method: 'notifications/resources/list_changed'; - params?: NotificationParams; -} - -/** - * The set of notification types a client may opt in to on a - * {@link SubscriptionsListenRequest | subscriptions/listen} request. - * - * Each notification type is **opt-in**; the server **MUST NOT** send - * notification types the client has not explicitly requested here. - * - * @category `subscriptions/listen` - */ -export interface SubscriptionFilter { - /** - * If true, receive {@link ToolListChangedNotification | notifications/tools/list_changed}. - */ - toolsListChanged?: boolean; - /** - * If true, receive {@link PromptListChangedNotification | notifications/prompts/list_changed}. - */ - promptsListChanged?: boolean; - /** - * If true, receive {@link ResourceListChangedNotification | notifications/resources/list_changed}. - */ - resourcesListChanged?: boolean; - /** - * Subscribe to {@link ResourceUpdatedNotification | notifications/resources/updated} for these resource URIs. - * Replaces the former `resources/subscribe` RPC. - */ - resourceSubscriptions?: string[]; -} - -/** - * Parameters for a {@link SubscriptionsListenRequest | subscriptions/listen} request. - * - * @category `subscriptions/listen` - */ -export interface SubscriptionsListenRequestParams extends RequestParams { - /** - * The notifications the client opts in to on this stream. The server - * **MUST NOT** send notification types the client has not explicitly - * requested. - */ - notifications: SubscriptionFilter; -} - -/** - * Sent from the client to open a long-lived channel for receiving notifications - * outside the context of a specific request. Replaces the previous HTTP GET - * endpoint and ensures consistent behavior between HTTP and STDIO. - * - * @example Listen for tools and resource list changes - * {@includeCode ./examples/SubscriptionsListenRequest/listen-for-list-changes.json} - * - * @category `subscriptions/listen` - */ -export interface SubscriptionsListenRequest extends JSONRPCRequest { - method: 'subscriptions/listen'; - params: SubscriptionsListenRequestParams; -} - -/** - * Parameters for a {@link SubscriptionsAcknowledgedNotification | notifications/subscriptions/acknowledged} notification. - * - * @category `notifications/subscriptions/acknowledged` - */ -export interface SubscriptionsAcknowledgedNotificationParams extends NotificationParams { - /** - * The subset of requested notification types the server agreed to honor. - * Only includes notification types the server actually supports; if the - * client requested an unsupported type (e.g., `promptsListChanged` when - * the server has no prompts), it is omitted from this set. - */ - notifications: SubscriptionFilter; -} - -/** - * Sent by the server as the first message on a - * {@link SubscriptionsListenRequest | subscriptions/listen} stream to acknowledge - * that the subscription has been established and to report which notification - * types it agreed to honor. - * - * @example Listen acknowledged - * {@includeCode ./examples/SubscriptionsAcknowledgedNotification/listen-acknowledged.json} - * - * @category `notifications/subscriptions/acknowledged` - */ -export interface SubscriptionsAcknowledgedNotification extends JSONRPCNotification { - method: 'notifications/subscriptions/acknowledged'; - params: SubscriptionsAcknowledgedNotificationParams; -} - -/** - * Parameters for a `notifications/resources/updated` notification. - * - * @example File resource updated - * {@includeCode ./examples/ResourceUpdatedNotificationParams/file-resource-updated.json} - * - * @category `notifications/resources/updated` - */ -export interface ResourceUpdatedNotificationParams extends NotificationParams { - /** - * The URI of the resource that has been updated. This might be a sub-resource of the one that the client actually subscribed to. - * - * @format uri - */ - uri: string; -} - -/** - * A notification from the server to the client, informing it that a resource has changed and may need to be read again. This is only sent for resources the client opted in to via the `resourceSubscriptions` field of a {@link SubscriptionsListenRequest | subscriptions/listen} request. - * - * @example File resource updated notification - * {@includeCode ./examples/ResourceUpdatedNotification/file-resource-updated-notification.json} - * - * @category `notifications/resources/updated` - */ -export interface ResourceUpdatedNotification extends JSONRPCNotification { - method: 'notifications/resources/updated'; - params: ResourceUpdatedNotificationParams; -} - -/** - * A known resource that the server is capable of reading. - * - * @example File resource with annotations - * {@includeCode ./examples/Resource/file-resource-with-annotations.json} - * - * @category `resources/list` - */ -export interface Resource extends BaseMetadata, Icons { - /** - * The URI of this resource. - * - * @format uri - */ - uri: string; - - /** - * A description of what this resource represents. - * - * This can be used by clients to improve the LLM's understanding of available resources. It can be thought of like a "hint" to the model. - */ - description?: string; - - /** - * The MIME type of this resource, if known. - */ - mimeType?: string; - - /** - * Optional annotations for the client. - */ - annotations?: Annotations; - - /** - * The size of the raw resource content, in bytes (i.e., before base64 encoding or any tokenization), if known. - * - * This can be used by Hosts to display file sizes and estimate context window usage. - */ - size?: number; - - _meta?: MetaObject; -} - -/** - * A template description for resources available on the server. - * - * @category `resources/templates/list` - */ -export interface ResourceTemplate extends BaseMetadata, Icons { - /** - * A URI template (according to RFC 6570) that can be used to construct resource URIs. - * - * @format uri-template - */ - uriTemplate: string; - - /** - * A description of what this template is for. - * - * This can be used by clients to improve the LLM's understanding of available resources. It can be thought of like a "hint" to the model. - */ - description?: string; - - /** - * The MIME type for all resources that match this template. This should only be included if all resources matching this template have the same type. - */ - mimeType?: string; - - /** - * Optional annotations for the client. - */ - annotations?: Annotations; - - _meta?: MetaObject; -} - -/** - * The contents of a specific resource or sub-resource. - * - * @internal - */ -export interface ResourceContents { - /** - * The URI of this resource. - * - * @format uri - */ - uri: string; - /** - * The MIME type of this resource, if known. - */ - mimeType?: string; - - _meta?: MetaObject; -} - -/** - * @example Text file contents - * {@includeCode ./examples/TextResourceContents/text-file-contents.json} - * - * @category Content - */ -export interface TextResourceContents extends ResourceContents { - /** - * The text of the item. This must only be set if the item can actually be represented as text (not binary data). - */ - text: string; -} - -/** - * @example Image file contents - * {@includeCode ./examples/BlobResourceContents/image-file-contents.json} - * - * @category Content - */ -export interface BlobResourceContents extends ResourceContents { - /** - * A base64-encoded string representing the binary data of the item. - * - * @format byte - */ - blob: string; -} - -/* Prompts */ -/** - * Sent from the client to request a list of prompts and prompt templates the server has. - * - * @example List prompts request - * {@includeCode ./examples/ListPromptsRequest/list-prompts-request.json} - * - * @category `prompts/list` - */ -export interface ListPromptsRequest extends PaginatedRequest { - method: 'prompts/list'; -} - -/** - * The result returned by the server for a {@link ListPromptsRequest | prompts/list} request. - * - * @example Prompts list with cursor and TTL - * {@includeCode ./examples/ListPromptsResult/prompts-list-with-cursor-and-ttl.json} - * - * @category `prompts/list` - */ -export interface ListPromptsResult extends PaginatedResult, CacheableResult { - prompts: Prompt[]; -} - -/** - * A successful response from the server for a {@link ListPromptsRequest | prompts/list} request. - * - * @example List prompts result response - * {@includeCode ./examples/ListPromptsResultResponse/list-prompts-result-response.json} - * - * @category `prompts/list` - */ -export interface ListPromptsResultResponse extends JSONRPCResultResponse { - result: ListPromptsResult; -} - -/** - * Parameters for a `prompts/get` request. - * - * @example Get code review prompt - * {@includeCode ./examples/GetPromptRequestParams/get-code-review-prompt.json} - * - * @category `prompts/get` - */ -export interface GetPromptRequestParams extends InputResponseRequestParams { - /** - * The name of the prompt or prompt template. - */ - name: string; - /** - * Arguments to use for templating the prompt. - */ - arguments?: { [key: string]: string }; -} - -/** - * Used by the client to get a prompt provided by the server. - * - * @example Get prompt request - * {@includeCode ./examples/GetPromptRequest/get-prompt-request.json} - * - * @category `prompts/get` - */ -export interface GetPromptRequest extends JSONRPCRequest { - method: 'prompts/get'; - params: GetPromptRequestParams; -} - -/** - * The result returned by the server for a {@link GetPromptRequest | prompts/get} request. - * - * @example Code review prompt - * {@includeCode ./examples/GetPromptResult/code-review-prompt.json} - * - * @category `prompts/get` - */ -export interface GetPromptResult extends Result { - /** - * An optional description for the prompt. - */ - description?: string; - messages: PromptMessage[]; -} - -/** - * A successful response from the server for a {@link GetPromptRequest | prompts/get} request. - * - * @example Get prompt result response - * {@includeCode ./examples/GetPromptResultResponse/get-prompt-result-response.json} - * - * @category `prompts/get` - */ -export interface GetPromptResultResponse extends JSONRPCResultResponse { - result: GetPromptResult | InputRequiredResult; -} - -/** - * A prompt or prompt template that the server offers. - * - * @category `prompts/list` - */ -export interface Prompt extends BaseMetadata, Icons { - /** - * An optional description of what this prompt provides - */ - description?: string; - - /** - * A list of arguments to use for templating the prompt. - */ - arguments?: PromptArgument[]; - - _meta?: MetaObject; -} - -/** - * Describes an argument that a prompt can accept. - * - * @category `prompts/list` - */ -export interface PromptArgument extends BaseMetadata { - /** - * A human-readable description of the argument. - */ - description?: string; - /** - * Whether this argument must be provided. - */ - required?: boolean; -} - -/** - * The sender or recipient of messages and data in a conversation. - * - * @category Common Types - */ -export type Role = 'user' | 'assistant'; - -/** - * Describes a message returned as part of a prompt. - * - * This is similar to {@link SamplingMessage}, but also supports the embedding of - * resources from the MCP server. - * - * @category `prompts/get` - */ -export interface PromptMessage { - role: Role; - content: ContentBlock; -} - -/** - * A resource that the server is capable of reading, included in a prompt or tool call result. - * - * Note: resource links returned by tools are not guaranteed to appear in the results of {@link ListResourcesRequest | resources/list} requests. - * - * @example File resource link - * {@includeCode ./examples/ResourceLink/file-resource-link.json} - * - * @category Content - */ -export interface ResourceLink extends Resource { - type: 'resource_link'; -} - -/** - * The contents of a resource, embedded into a prompt or tool call result. - * - * It is up to the client how best to render embedded resources for the benefit - * of the LLM and/or the user. - * - * @example Embedded file resource with annotations - * {@includeCode ./examples/EmbeddedResource/embedded-file-resource-with-annotations.json} - * - * @category Content - */ -export interface EmbeddedResource { - type: 'resource'; - resource: TextResourceContents | BlobResourceContents; - - /** - * Optional annotations for the client. - */ - annotations?: Annotations; - - _meta?: MetaObject; -} -/** - * An optional notification from the server to the client, informing it that the list of prompts it offers has changed. This may be issued by servers without any previous subscription from the client. - * - * @example Prompts list changed - * {@includeCode ./examples/PromptListChangedNotification/prompts-list-changed.json} - * - * @category `notifications/prompts/list_changed` - */ -export interface PromptListChangedNotification extends JSONRPCNotification { - method: 'notifications/prompts/list_changed'; - params?: NotificationParams; -} - -/* Tools */ -/** - * Sent from the client to request a list of tools the server has. - * - * @example List tools request - * {@includeCode ./examples/ListToolsRequest/list-tools-request.json} - * - * @category `tools/list` - */ -export interface ListToolsRequest extends PaginatedRequest { - method: 'tools/list'; -} - -/** - * The result returned by the server for a {@link ListToolsRequest | tools/list} request. - * - * @example Tools list with cursor and TTL - * {@includeCode ./examples/ListToolsResult/tools-list-with-cursor-and-ttl.json} - * - * @category `tools/list` - */ -export interface ListToolsResult extends PaginatedResult, CacheableResult { - tools: Tool[]; -} - -/** - * A successful response from the server for a {@link ListToolsRequest | tools/list} request. - * - * @example List tools result response - * {@includeCode ./examples/ListToolsResultResponse/list-tools-result-response.json} - * - * @category `tools/list` - */ -export interface ListToolsResultResponse extends JSONRPCResultResponse { - result: ListToolsResult; -} - -/** - * The result returned by the server for a {@link CallToolRequest | tools/call} request. - * - * @example Result with unstructured text - * {@includeCode ./examples/CallToolResult/result-with-unstructured-text.json} - * - * @example Result with structured content - * {@includeCode ./examples/CallToolResult/result-with-structured-content.json} - * - * @example Invalid tool input error - * {@includeCode ./examples/CallToolResult/invalid-tool-input-error.json} - * - * @category `tools/call` - */ -export interface CallToolResult extends Result { - /** - * A list of content objects that represent the unstructured result of the tool call. - */ - content: ContentBlock[]; - - /** - * An optional JSON value that represents the structured result of the tool call. - * - * This can be any JSON value (object, array, string, number, boolean, or null) - * that conforms to the tool's outputSchema if one is defined. - */ - structuredContent?: unknown; - - /** - * Whether the tool call ended in an error. - * - * If not set, this is assumed to be false (the call was successful). - * - * Any errors that originate from the tool SHOULD be reported inside the result - * object, with `isError` set to true, _not_ as an MCP protocol-level error - * response. Otherwise, the LLM would not be able to see that an error occurred - * and self-correct. - * - * However, any errors in _finding_ the tool, an error indicating that the - * server does not support tool calls, or any other exceptional conditions, - * should be reported as an MCP error response. - */ - isError?: boolean; -} - -/** - * A successful response from the server for a {@link CallToolRequest | tools/call} request. - * - * @example Call tool result response - * {@includeCode ./examples/CallToolResultResponse/call-tool-result-response.json} - * - * @category `tools/call` - */ -export interface CallToolResultResponse extends JSONRPCResultResponse { - result: CallToolResult | InputRequiredResult; -} - -/** - * Parameters for a `tools/call` request. - * - * @example `get_weather` tool call params - * {@includeCode ./examples/CallToolRequestParams/get-weather-tool-call-params.json} - * - * @example Tool call params with progress token - * {@includeCode ./examples/CallToolRequestParams/tool-call-params-with-progress-token.json} - * - * @category `tools/call` - */ -export interface CallToolRequestParams extends InputResponseRequestParams { - /** - * The name of the tool. - */ - name: string; - /** - * Arguments to use for the tool call. - */ - arguments?: { [key: string]: unknown }; -} - -/** - * Used by the client to invoke a tool provided by the server. - * - * @example Call tool request - * {@includeCode ./examples/CallToolRequest/call-tool-request.json} - * - * @category `tools/call` - */ -export interface CallToolRequest extends JSONRPCRequest { - method: 'tools/call'; - params: CallToolRequestParams; -} - -/** - * An optional notification from the server to the client, informing it that the list of tools it offers has changed. This may be issued by servers without any previous subscription from the client. - * - * @example Tools list changed - * {@includeCode ./examples/ToolListChangedNotification/tools-list-changed.json} - * - * @category `notifications/tools/list_changed` - */ -export interface ToolListChangedNotification extends JSONRPCNotification { - method: 'notifications/tools/list_changed'; - params?: NotificationParams; -} - -/** - * Additional properties describing a {@link Tool} to clients. - * - * NOTE: all properties in `ToolAnnotations` are **hints**. - * They are not guaranteed to provide a faithful description of - * tool behavior (including descriptive properties like `title`). - * - * Clients should never make tool use decisions based on `ToolAnnotations` - * received from untrusted servers. - * - * @category `tools/list` - */ -export interface ToolAnnotations { - /** - * A human-readable title for the tool. - */ - title?: string; - - /** - * If true, the tool does not modify its environment. - * - * Default: false - */ - readOnlyHint?: boolean; - - /** - * If true, the tool may perform destructive updates to its environment. - * If false, the tool performs only additive updates. - * - * (This property is meaningful only when `readOnlyHint == false`) - * - * Default: true - */ - destructiveHint?: boolean; - - /** - * If true, calling the tool repeatedly with the same arguments - * will have no additional effect on its environment. - * - * (This property is meaningful only when `readOnlyHint == false`) - * - * Default: false - */ - idempotentHint?: boolean; - - /** - * If true, this tool may interact with an "open world" of external - * entities. If false, the tool's domain of interaction is closed. - * For example, the world of a web search tool is open, whereas that - * of a memory tool is not. - * - * Default: true - */ - openWorldHint?: boolean; -} - -/** - * Definition for a tool the client can call. - * - * @example With default 2020-12 input schema - * {@includeCode ./examples/Tool/with-default-2020-12-input-schema.json} - * - * @example With explicit draft-07 input schema - * {@includeCode ./examples/Tool/with-explicit-draft-07-input-schema.json} - * - * @example With no parameters - * {@includeCode ./examples/Tool/with-no-parameters.json} - * - * @example With output schema for structured content - * {@includeCode ./examples/Tool/with-output-schema-for-structured-content.json} - * - * @category `tools/list` - */ -export interface Tool extends BaseMetadata, Icons { - /** - * A human-readable description of the tool. - * - * This can be used by clients to improve the LLM's understanding of available tools. It can be thought of like a "hint" to the model. - */ - description?: string; - - /** - * A JSON Schema object defining the expected parameters for the tool. - * - * Tool arguments are always JSON objects, so `type: "object"` is required at the root. - * Beyond that, any JSON Schema 2020-12 keyword may appear alongside `type` — including - * composition keywords (`oneOf`, `anyOf`, `allOf`, `not`), conditional keywords - * (`if`/`then`/`else`), reference keywords (`$ref`, `$defs`, `$anchor`), and any other - * standard validation or annotation keywords. - * - * Defaults to JSON Schema 2020-12 when no explicit `$schema` is provided. - */ - inputSchema: { $schema?: string; type: 'object'; [key: string]: unknown }; - - /** - * An optional JSON Schema object defining the structure of the tool's output returned in - * the structuredContent field of a {@link CallToolResult}. This can be any valid JSON Schema 2020-12. - * - * Defaults to JSON Schema 2020-12 when no explicit `$schema` is provided. - */ - outputSchema?: { $schema?: string; [key: string]: unknown }; - - /** - * Optional additional tool information. - * - * Display name precedence order is: `title`, `annotations.title`, then `name`. - */ - annotations?: ToolAnnotations; - - _meta?: MetaObject; -} - -/* Logging */ - -/** - * Parameters for a `notifications/message` notification. - * - * @deprecated Deprecated as of protocol version 2026-07-28 (SEP-2577). - * Remains in the specification for at least twelve months; see the - * deprecated features registry. - * - * @example Log database connection failed - * {@includeCode ./examples/LoggingMessageNotificationParams/log-database-connection-failed.json} - * - * @category `notifications/message` - */ -export interface LoggingMessageNotificationParams extends NotificationParams { - /** - * The severity of this log message. - */ - level: LoggingLevel; - /** - * An optional name of the logger issuing this message. - */ - logger?: string; - /** - * The data to be logged, such as a string message or an object. Any JSON serializable type is allowed here. - */ - data: unknown; -} - -/** - * JSONRPCNotification of a log message passed from server to client. The client opts in by setting `"io.modelcontextprotocol/logLevel"` in a request's `_meta`. - * - * @deprecated Deprecated as of protocol version 2026-07-28 (SEP-2577). - * Remains in the specification for at least twelve months; see the - * deprecated features registry. - * - * @example Log database connection failed - * {@includeCode ./examples/LoggingMessageNotification/log-database-connection-failed.json} - * - * @category `notifications/message` - */ -export interface LoggingMessageNotification extends JSONRPCNotification { - method: 'notifications/message'; - params: LoggingMessageNotificationParams; -} - -/** - * The severity of a log message. - * - * These map to syslog message severities, as specified in RFC-5424: - * https://datatracker.ietf.org/doc/html/rfc5424#section-6.2.1 - * - * @deprecated Deprecated as of protocol version 2026-07-28 (SEP-2577). - * Remains in the specification for at least twelve months; see the - * deprecated features registry. - * - * @category Common Types - */ -export type LoggingLevel = 'debug' | 'info' | 'notice' | 'warning' | 'error' | 'critical' | 'alert' | 'emergency'; - -/* Sampling */ -/** - * Parameters for a `sampling/createMessage` request. - * - * @example Basic request - * {@includeCode ./examples/CreateMessageRequestParams/basic-request.json} - * - * @example Request with tools - * {@includeCode ./examples/CreateMessageRequestParams/request-with-tools.json} - * - * @example Follow-up request with tool results - * {@includeCode ./examples/CreateMessageRequestParams/follow-up-with-tool-results.json} - * - * @deprecated Deprecated as of protocol version 2026-07-28 (SEP-2577). - * Remains in the specification for at least twelve months; see the - * deprecated features registry. - * - * @category `sampling/createMessage` - */ -export interface CreateMessageRequestParams { - messages: SamplingMessage[]; - /** - * The server's preferences for which model to select. The client MAY ignore these preferences. - */ - modelPreferences?: ModelPreferences; - /** - * An optional system prompt the server wants to use for sampling. The client MAY modify or omit this prompt. - */ - systemPrompt?: string; - /** - * A request to include context from one or more MCP servers (including the caller), to be attached to the prompt. - * The client MAY ignore this request. - * - * Default is `"none"`. The values `"thisServer"` and `"allServers"` are deprecated (SEP-2596): servers SHOULD - * omit this field or use `"none"`, and SHOULD only use the deprecated values if the client declares - * {@link ClientCapabilities.sampling.context}. - * - * @deprecated The `"thisServer"` and `"allServers"` values are deprecated as of protocol version 2025-11-25 - * (SEP-2596) and will be removed no later than the Sampling feature itself (SEP-2577). Omit this field or use `"none"`. - */ - includeContext?: 'none' | 'thisServer' | 'allServers'; - /** - * @TJS-type number - */ - temperature?: number; - /** - * The requested maximum number of tokens to sample (to prevent runaway completions). - * - * The client MAY choose to sample fewer tokens than the requested maximum. - */ - maxTokens: number; - stopSequences?: string[]; - /** - * Optional metadata to pass through to the LLM provider. The format of this metadata is provider-specific. - */ - metadata?: JSONObject; - /** - * Tools that the model may use during generation. - * The client MUST return an error if this field is provided but {@link ClientCapabilities.sampling.tools} is not declared. - */ - tools?: Tool[]; - /** - * Controls how the model uses tools. - * The client MUST return an error if this field is provided but {@link ClientCapabilities.sampling.tools} is not declared. - * Default is `{ mode: "auto" }`. - */ - toolChoice?: ToolChoice; -} - -/** - * Controls tool selection behavior for sampling requests. - * - * @deprecated Deprecated as of protocol version 2026-07-28 (SEP-2577). - * Remains in the specification for at least twelve months; see the - * deprecated features registry. - * - * @category `sampling/createMessage` - */ -export interface ToolChoice { - /** - * Controls the tool use ability of the model: - * - `"auto"`: Model decides whether to use tools (default) - * - `"required"`: Model MUST use at least one tool before completing - * - `"none"`: Model MUST NOT use any tools - */ - mode?: 'auto' | 'required' | 'none'; -} - -/** - * A request from the server to sample an LLM via the client. The client has full discretion over which model to select. The client should also inform the user before beginning sampling, to allow them to inspect the request (human in the loop) and decide whether to approve it. - * - * @example Sampling request - * {@includeCode ./examples/CreateMessageRequest/sampling-request.json} - * - * @deprecated Deprecated as of protocol version 2026-07-28 (SEP-2577). - * Remains in the specification for at least twelve months; see the - * deprecated features registry. - * - * @category `sampling/createMessage` - */ -export interface CreateMessageRequest { - method: 'sampling/createMessage'; - params: CreateMessageRequestParams; -} - -/** - * The result returned by the client for a {@link CreateMessageRequest | sampling/createMessage} request. - * The client should inform the user before returning the sampled message, to allow them - * to inspect the response (human in the loop) and decide whether to allow the server to see it. - * - * @example Text response - * {@includeCode ./examples/CreateMessageResult/text-response.json} - * - * @example Tool use response - * {@includeCode ./examples/CreateMessageResult/tool-use-response.json} - * - * @example Final response after tool use - * {@includeCode ./examples/CreateMessageResult/final-response.json} - * - * @deprecated Deprecated as of protocol version 2026-07-28 (SEP-2577). - * Remains in the specification for at least twelve months; see the - * deprecated features registry. - * - * @category `sampling/createMessage` - */ -export interface CreateMessageResult extends SamplingMessage { - /** - * The name of the model that generated the message. - */ - model: string; - - /** - * The reason why sampling stopped, if known. - * - * Standard values: - * - `"endTurn"`: Natural end of the assistant's turn - * - `"stopSequence"`: A stop sequence was encountered - * - `"maxTokens"`: Maximum token limit was reached - * - `"toolUse"`: The model wants to use one or more tools - * - * This field is an open string to allow for provider-specific stop reasons. - */ - stopReason?: 'endTurn' | 'stopSequence' | 'maxTokens' | 'toolUse' | string; -} - -/** - * Describes a message issued to or received from an LLM API. - * - * @example Single content block - * {@includeCode ./examples/SamplingMessage/single-content-block.json} - * - * @example Multiple content blocks - * {@includeCode ./examples/SamplingMessage/multiple-content-blocks.json} - * - * @deprecated Deprecated as of protocol version 2026-07-28 (SEP-2577). - * Remains in the specification for at least twelve months; see the - * deprecated features registry. - * - * @category `sampling/createMessage` - */ -export interface SamplingMessage { - role: Role; - content: SamplingMessageContentBlock | SamplingMessageContentBlock[]; - _meta?: MetaObject; -} - -/** - * @deprecated Deprecated as of protocol version 2026-07-28 (SEP-2577). - * Remains in the specification for at least twelve months; see the - * deprecated features registry. - * - * @category `sampling/createMessage` - */ -export type SamplingMessageContentBlock = TextContent | ImageContent | AudioContent | ToolUseContent | ToolResultContent; - -/** - * Optional annotations for the client. The client can use annotations to inform how objects are used or displayed - * - * @category Common Types - */ -export interface Annotations { - /** - * Describes who the intended audience of this object or data is. - * - * It can include multiple entries to indicate content useful for multiple audiences (e.g., `["user", "assistant"]`). - */ - audience?: Role[]; - - /** - * Describes how important this data is for operating the server. - * - * A value of 1 means "most important," and indicates that the data is - * effectively required, while 0 means "least important," and indicates that - * the data is entirely optional. - * - * @TJS-type number - * @minimum 0 - * @maximum 1 - */ - priority?: number; - - /** - * The moment the resource was last modified, as an ISO 8601 formatted string. - * - * Should be an ISO 8601 formatted string (e.g., "2025-01-12T15:00:58Z"). - * - * Examples: last activity timestamp in an open file, timestamp when the resource - * was attached, etc. - */ - lastModified?: string; -} - -/** - * @category Content - */ -export type ContentBlock = TextContent | ImageContent | AudioContent | ResourceLink | EmbeddedResource; - -/** - * Text provided to or from an LLM. - * - * @example Text content - * {@includeCode ./examples/TextContent/text-content.json} - * - * @category Content - */ -export interface TextContent { - type: 'text'; - - /** - * The text content of the message. - */ - text: string; - - /** - * Optional annotations for the client. - */ - annotations?: Annotations; - - _meta?: MetaObject; -} - -/** - * An image provided to or from an LLM. - * - * @example `image/png` content with annotations - * {@includeCode ./examples/ImageContent/image-png-content-with-annotations.json} - * - * @category Content - */ -export interface ImageContent { - type: 'image'; - - /** - * The base64-encoded image data. - * - * @format byte - */ - data: string; - - /** - * The MIME type of the image. Different providers may support different image types. - */ - mimeType: string; - - /** - * Optional annotations for the client. - */ - annotations?: Annotations; - - _meta?: MetaObject; -} - -/** - * Audio provided to or from an LLM. - * - * @example `audio/wav` content - * {@includeCode ./examples/AudioContent/audio-wav-content.json} - * - * @category Content - */ -export interface AudioContent { - type: 'audio'; - - /** - * The base64-encoded audio data. - * - * @format byte - */ - data: string; - - /** - * The MIME type of the audio. Different providers may support different audio types. - */ - mimeType: string; - - /** - * Optional annotations for the client. - */ - annotations?: Annotations; - - _meta?: MetaObject; -} - -/** - * A request from the assistant to call a tool. - * - * @example `get_weather` tool use - * {@includeCode ./examples/ToolUseContent/get-weather-tool-use.json} - * - * @deprecated Deprecated as of protocol version 2026-07-28 (SEP-2577). - * Remains in the specification for at least twelve months; see the - * deprecated features registry. - * - * @category `sampling/createMessage` - */ -export interface ToolUseContent { - type: 'tool_use'; - - /** - * A unique identifier for this tool use. - * - * This ID is used to match tool results to their corresponding tool uses. - */ - id: string; - - /** - * The name of the tool to call. - */ - name: string; - - /** - * The arguments to pass to the tool, conforming to the tool's input schema. - */ - input: { [key: string]: unknown }; - - /** - * Optional metadata about the tool use. Clients SHOULD preserve this field when - * including tool uses in subsequent sampling requests to enable caching optimizations. - */ - _meta?: MetaObject; -} - -/** - * The result of a tool use, provided by the user back to the assistant. - * - * @example `get_weather` tool result - * {@includeCode ./examples/ToolResultContent/get-weather-tool-result.json} - * - * @deprecated Deprecated as of protocol version 2026-07-28 (SEP-2577). - * Remains in the specification for at least twelve months; see the - * deprecated features registry. - * - * @category `sampling/createMessage` - */ -export interface ToolResultContent { - type: 'tool_result'; - - /** - * The ID of the tool use this result corresponds to. - * - * This MUST match the ID from a previous {@link ToolUseContent}. - */ - toolUseId: string; - - /** - * The unstructured result content of the tool use. - * - * This has the same format as {@link CallToolResult.content} and can include text, images, - * audio, resource links, and embedded resources. - */ - content: ContentBlock[]; - - /** - * An optional structured result value. - * - * This can be any JSON value (object, array, string, number, boolean, or null). - * If the tool defined an {@link Tool.outputSchema}, this SHOULD conform to that schema. - */ - structuredContent?: unknown; - - /** - * Whether the tool use resulted in an error. - * - * If true, the content typically describes the error that occurred. - * Default: false - */ - isError?: boolean; - - /** - * Optional metadata about the tool result. Clients SHOULD preserve this field when - * including tool results in subsequent sampling requests to enable caching optimizations. - */ - _meta?: MetaObject; -} - -/** - * The server's preferences for model selection, requested of the client during sampling. - * - * Because LLMs can vary along multiple dimensions, choosing the "best" model is - * rarely straightforward. Different models excel in different areas—some are - * faster but less capable, others are more capable but more expensive, and so - * on. This interface allows servers to express their priorities across multiple - * dimensions to help clients make an appropriate selection for their use case. - * - * These preferences are always advisory. The client MAY ignore them. It is also - * up to the client to decide how to interpret these preferences and how to - * balance them against other considerations. - * - * @example With hints and priorities - * {@includeCode ./examples/ModelPreferences/with-hints-and-priorities.json} - * - * @deprecated Deprecated as of protocol version 2026-07-28 (SEP-2577). - * Remains in the specification for at least twelve months; see the - * deprecated features registry. - * - * @category `sampling/createMessage` - */ -export interface ModelPreferences { - /** - * Optional hints to use for model selection. - * - * If multiple hints are specified, the client MUST evaluate them in order - * (such that the first match is taken). - * - * The client SHOULD prioritize these hints over the numeric priorities, but - * MAY still use the priorities to select from ambiguous matches. - */ - hints?: ModelHint[]; - - /** - * How much to prioritize cost when selecting a model. A value of 0 means cost - * is not important, while a value of 1 means cost is the most important - * factor. - * - * @TJS-type number - * @minimum 0 - * @maximum 1 - */ - costPriority?: number; - - /** - * How much to prioritize sampling speed (latency) when selecting a model. A - * value of 0 means speed is not important, while a value of 1 means speed is - * the most important factor. - * - * @TJS-type number - * @minimum 0 - * @maximum 1 - */ - speedPriority?: number; - - /** - * How much to prioritize intelligence and capabilities when selecting a - * model. A value of 0 means intelligence is not important, while a value of 1 - * means intelligence is the most important factor. - * - * @TJS-type number - * @minimum 0 - * @maximum 1 - */ - intelligencePriority?: number; -} - -/** - * Hints to use for model selection. - * - * Keys not declared here are currently left unspecified by the spec and are up - * to the client to interpret. - * - * @deprecated Deprecated as of protocol version 2026-07-28 (SEP-2577). - * Remains in the specification for at least twelve months; see the - * deprecated features registry. - * - * @category `sampling/createMessage` - */ -export interface ModelHint { - /** - * A hint for a model name. - * - * The client SHOULD treat this as a substring of a model name; for example: - * - `claude-3-5-sonnet` should match `claude-3-5-sonnet-20241022` - * - `sonnet` should match `claude-3-5-sonnet-20241022`, `claude-3-sonnet-20240229`, etc. - * - `claude` should match any Claude model - * - * The client MAY also map the string to a different provider's model name or a different model family, as long as it fills a similar niche; for example: - * - `gemini-1.5-flash` could match `claude-3-haiku-20240307` - */ - name?: string; -} - -/* Autocomplete */ -/** - * Parameters for a `completion/complete` request. - * - * @category `completion/complete` - * - * @example Prompt argument completion - * {@includeCode ./examples/CompleteRequestParams/prompt-argument-completion.json} - * - * @example Prompt argument completion with context - * {@includeCode ./examples/CompleteRequestParams/prompt-argument-completion-with-context.json} - */ -export interface CompleteRequestParams extends RequestParams { - ref: PromptReference | ResourceTemplateReference; - /** - * The argument's information - */ - argument: { - /** - * The name of the argument - */ - name: string; - /** - * The value of the argument to use for completion matching. - */ - value: string; - }; - - /** - * Additional, optional context for completions - */ - context?: { - /** - * Previously-resolved variables in a URI template or prompt. - */ - arguments?: { [key: string]: string }; - }; -} - -/** - * A request from the client to the server, to ask for completion options. - * - * @example Completion request - * {@includeCode ./examples/CompleteRequest/completion-request.json} - * - * @category `completion/complete` - */ -export interface CompleteRequest extends JSONRPCRequest { - method: 'completion/complete'; - params: CompleteRequestParams; -} - -/** - * The result returned by the server for a {@link CompleteRequest | completion/complete} request. - * - * @category `completion/complete` - * - * @example Single completion value - * {@includeCode ./examples/CompleteResult/single-completion-value.json} - * - * @example Multiple completion values with more available - * {@includeCode ./examples/CompleteResult/multiple-completion-values-with-more-available.json} - */ -export interface CompleteResult extends Result { - completion: { - /** - * An array of completion values. Must not exceed 100 items. - * - * @maxItems 100 - */ - values: string[]; - /** - * The total number of completion options available. This can exceed the number of values actually sent in the response. - */ - total?: number; - /** - * Indicates whether there are additional completion options beyond those provided in the current response, even if the exact total is unknown. - */ - hasMore?: boolean; - }; -} - -/** - * A successful response from the server for a {@link CompleteRequest | completion/complete} request. - * - * @example Completion result response - * {@includeCode ./examples/CompleteResultResponse/completion-result-response.json} - * - * @category `completion/complete` - */ -export interface CompleteResultResponse extends JSONRPCResultResponse { - result: CompleteResult; -} - -/** - * A reference to a resource or resource template definition. - * - * @category `completion/complete` - */ -export interface ResourceTemplateReference { - type: 'ref/resource'; - /** - * The URI or URI template of the resource. - * - * @format uri-template - */ - uri: string; -} - -/** - * Identifies a prompt. - * - * @category `completion/complete` - */ -export interface PromptReference extends BaseMetadata { - type: 'ref/prompt'; -} - -/* Roots */ -/** - * Sent from the server to request a list of root URIs from the client. Roots allow - * servers to ask for specific directories or files to operate on. A common example - * for roots is providing a set of repositories or directories a server should operate - * on. - * - * This request is typically used when the server needs to understand the file system - * structure or access specific locations that the client has permission to read from. - * - * @example List roots request - * {@includeCode ./examples/ListRootsRequest/list-roots-request.json} - * - * @deprecated Deprecated as of protocol version 2026-07-28 (SEP-2577). - * Remains in the specification for at least twelve months; see the - * deprecated features registry. - * - * @category `roots/list` - */ -export interface ListRootsRequest { - method: 'roots/list'; - params?: RequestParams; -} - -/** - * The result returned by the client for a {@link ListRootsRequest | roots/list} request. - * This result contains an array of {@link Root} objects, each representing a root directory - * or file that the server can operate on. - * - * @example Single root directory - * {@includeCode ./examples/ListRootsResult/single-root-directory.json} - * - * @example Multiple root directories - * {@includeCode ./examples/ListRootsResult/multiple-root-directories.json} - * - * @deprecated Deprecated as of protocol version 2026-07-28 (SEP-2577). - * Remains in the specification for at least twelve months; see the - * deprecated features registry. - * - * @category `roots/list` - */ -export interface ListRootsResult { - roots: Root[]; -} - -/** - * Represents a root directory or file that the server can operate on. - * - * @example Project directory root - * {@includeCode ./examples/Root/project-directory.json} - * - * @deprecated Deprecated as of protocol version 2026-07-28 (SEP-2577). - * Remains in the specification for at least twelve months; see the - * deprecated features registry. - * - * @category `roots/list` - */ -export interface Root { - /** - * The URI identifying the root. This *must* start with `file://` for now. - * This restriction may be relaxed in future versions of the protocol to allow - * other URI schemes. - * - * @format uri - */ - uri: string; - /** - * An optional name for the root. This can be used to provide a human-readable - * identifier for the root, which may be useful for display purposes or for - * referencing the root in other parts of the application. - */ - name?: string; - - _meta?: MetaObject; -} - -/** - * The parameters for a request to elicit non-sensitive information from the user via a form in the client. - * - * @example Elicit single field - * {@includeCode ./examples/ElicitRequestFormParams/elicit-single-field.json} - * - * @example Elicit multiple fields - * {@includeCode ./examples/ElicitRequestFormParams/elicit-multiple-fields.json} - * - * @category `elicitation/create` - */ -export interface ElicitRequestFormParams { - /** - * The elicitation mode. - */ - mode?: 'form'; - - /** - * The message to present to the user describing what information is being requested. - */ - message: string; - - /** - * A restricted subset of JSON Schema. - * Only top-level properties are allowed, without nesting. - */ - requestedSchema: { - $schema?: string; - type: 'object'; - properties: { - [key: string]: PrimitiveSchemaDefinition; - }; - required?: string[]; - }; -} - -/** - * The parameters for a request to elicit information from the user via a URL in the client. - * - * @example Elicit sensitive data - * {@includeCode ./examples/ElicitRequestURLParams/elicit-sensitive-data.json} - * - * @category `elicitation/create` - */ -export interface ElicitRequestURLParams { - /** - * The elicitation mode. - */ - mode: 'url'; - - /** - * The message to present to the user explaining why the interaction is needed. - */ - message: string; - - /** - * The ID of the elicitation, which must be unique within the context of the server. - * The client MUST treat this ID as an opaque value. - */ - elicitationId: string; - - /** - * The URL that the user should navigate to. - * - * @format uri - */ - url: string; -} - -/** - * The parameters for a request to elicit additional information from the user via the client. - * - * @category `elicitation/create` - */ -export type ElicitRequestParams = ElicitRequestFormParams | ElicitRequestURLParams; - -/** - * A request from the server to elicit additional information from the user via the client. - * - * @example Elicitation request - * {@includeCode ./examples/ElicitRequest/elicitation-request.json} - * - * @category `elicitation/create` - */ -export interface ElicitRequest { - method: 'elicitation/create'; - params: ElicitRequestParams; -} - -/** - * Restricted schema definitions that only allow primitive types - * without nested objects or arrays. - * - * @category `elicitation/create` - */ -export type PrimitiveSchemaDefinition = StringSchema | NumberSchema | BooleanSchema | EnumSchema; - -/** - * @example Email input schema - * {@includeCode ./examples/StringSchema/email-input-schema.json} - * - * @category `elicitation/create` - */ -export interface StringSchema { - type: 'string'; - title?: string; - description?: string; - minLength?: number; - maxLength?: number; - format?: 'email' | 'uri' | 'date' | 'date-time'; - default?: string; -} - -/** - * @example Number input schema - * {@includeCode ./examples/NumberSchema/number-input-schema.json} - * - * @category `elicitation/create` - */ -export interface NumberSchema { - type: 'number' | 'integer'; - title?: string; - description?: string; - /** - * @TJS-type number - */ - minimum?: number; - /** - * @TJS-type number - */ - maximum?: number; - /** - * @TJS-type number - */ - default?: number; -} - -/** - * @example Boolean input schema - * {@includeCode ./examples/BooleanSchema/boolean-input-schema.json} - * - * @category `elicitation/create` - */ -export interface BooleanSchema { - type: 'boolean'; - title?: string; - description?: string; - default?: boolean; -} - -/** - * Schema for single-selection enumeration without display titles for options. - * - * @example Color select schema - * {@includeCode ./examples/UntitledSingleSelectEnumSchema/color-select-schema.json} - * - * @category `elicitation/create` - */ -export interface UntitledSingleSelectEnumSchema { - type: 'string'; - /** - * Optional title for the enum field. - */ - title?: string; - /** - * Optional description for the enum field. - */ - description?: string; - /** - * Array of enum values to choose from. - */ - enum: string[]; - /** - * Optional default value. - */ - default?: string; -} - -/** - * Schema for single-selection enumeration with display titles for each option. - * - * @example Titled color select schema - * {@includeCode ./examples/TitledSingleSelectEnumSchema/titled-color-select-schema.json} - * - * @category `elicitation/create` - */ -export interface TitledSingleSelectEnumSchema { - type: 'string'; - /** - * Optional title for the enum field. - */ - title?: string; - /** - * Optional description for the enum field. - */ - description?: string; - /** - * Array of enum options with values and display labels. - */ - oneOf: Array<{ - /** - * The enum value. - */ - const: string; - /** - * Display label for this option. - */ - title: string; - }>; - /** - * Optional default value. - */ - default?: string; -} - -/** - * @category `elicitation/create` - */ -// Combined single selection enumeration -export type SingleSelectEnumSchema = UntitledSingleSelectEnumSchema | TitledSingleSelectEnumSchema; - -/** - * Schema for multiple-selection enumeration without display titles for options. - * - * @example Color multi-select schema - * {@includeCode ./examples/UntitledMultiSelectEnumSchema/color-multi-select-schema.json} - * - * @category `elicitation/create` - */ -export interface UntitledMultiSelectEnumSchema { - type: 'array'; - /** - * Optional title for the enum field. - */ - title?: string; - /** - * Optional description for the enum field. - */ - description?: string; - /** - * Minimum number of items to select. - */ - minItems?: number; - /** - * Maximum number of items to select. - */ - maxItems?: number; - /** - * Schema for the array items. - */ - items: { - type: 'string'; - /** - * Array of enum values to choose from. - */ - enum: string[]; - }; - /** - * Optional default value. - */ - default?: string[]; -} - -/** - * Schema for multiple-selection enumeration with display titles for each option. - * - * @example Titled color multi-select schema - * {@includeCode ./examples/TitledMultiSelectEnumSchema/titled-color-multi-select-schema.json} - * - * @category `elicitation/create` - */ -export interface TitledMultiSelectEnumSchema { - type: 'array'; - /** - * Optional title for the enum field. - */ - title?: string; - /** - * Optional description for the enum field. - */ - description?: string; - /** - * Minimum number of items to select. - */ - minItems?: number; - /** - * Maximum number of items to select. - */ - maxItems?: number; - /** - * Schema for array items with enum options and display labels. - */ - items: { - /** - * Array of enum options with values and display labels. - */ - anyOf: Array<{ - /** - * The constant enum value. - */ - const: string; - /** - * Display title for this option. - */ - title: string; - }>; - }; - /** - * Optional default value. - */ - default?: string[]; -} - -/** - * @category `elicitation/create` - */ -// Combined multiple selection enumeration -export type MultiSelectEnumSchema = UntitledMultiSelectEnumSchema | TitledMultiSelectEnumSchema; - -/** - * Use {@link TitledSingleSelectEnumSchema} instead. - * This interface will be removed in a future version. - * - * @category `elicitation/create` - */ -export interface LegacyTitledEnumSchema { - type: 'string'; - title?: string; - description?: string; - enum: string[]; - /** - * (Legacy) Display names for enum values. - * Non-standard according to JSON schema 2020-12. - */ - enumNames?: string[]; - default?: string; -} - -/** - * @category `elicitation/create` - */ -// Union type for all enum schemas -export type EnumSchema = SingleSelectEnumSchema | MultiSelectEnumSchema | LegacyTitledEnumSchema; - -/** - * The result returned by the client for an {@link ElicitRequest| elicitation/create} request. - * - * @example Input single field - * {@includeCode ./examples/ElicitResult/input-single-field.json} - * - * @example Input multiple fields - * {@includeCode ./examples/ElicitResult/input-multiple-fields.json} - * - * @example Accept URL mode (no content) - * {@includeCode ./examples/ElicitResult/accept-url-mode-no-content.json} - * - * @category `elicitation/create` - */ -export interface ElicitResult { - /** - * The user action in response to the elicitation. - * - `"accept"`: User submitted the form/confirmed the action - * - `"decline"`: User explicitly declined the action - * - `"cancel"`: User dismissed without making an explicit choice - */ - action: 'accept' | 'decline' | 'cancel'; - - /** - * The submitted form data, only present when action is `"accept"` and mode was `"form"`. - * Contains values matching the requested schema. - * Omitted for out-of-band mode responses. - */ - content?: { [key: string]: string | number | boolean | string[] }; -} - -/** - * An optional notification from the server to the client, informing it of a completion of a out-of-band elicitation request. - * - * @example Elicitation complete - * {@includeCode ./examples/ElicitationCompleteNotification/elicitation-complete.json} - * - * @category `notifications/elicitation/complete` - */ -export interface ElicitationCompleteNotification extends JSONRPCNotification { - method: 'notifications/elicitation/complete'; - params: { - /** - * The ID of the elicitation that completed. - */ - elicitationId: string; - }; -} - -/* Client messages */ -/** @internal */ -export type ClientRequest = - | DiscoverRequest - | CompleteRequest - | GetPromptRequest - | ListPromptsRequest - | ListResourcesRequest - | ListResourceTemplatesRequest - | ReadResourceRequest - | SubscriptionsListenRequest - | CallToolRequest - | ListToolsRequest; - -/** @internal */ -export type ClientNotification = CancelledNotification | ProgressNotification; - -/** @internal */ -export type ClientResult = EmptyResult; - -/* Server messages */ - -/** @internal */ -export type ServerNotification = - | CancelledNotification - | ProgressNotification - | LoggingMessageNotification - | ResourceUpdatedNotification - | ResourceListChangedNotification - | ToolListChangedNotification - | PromptListChangedNotification - | ElicitationCompleteNotification - | SubscriptionsAcknowledgedNotification; - -/** @internal */ -export type ServerResult = - | EmptyResult - | DiscoverResult - | CompleteResult - | GetPromptResult - | ListPromptsResult - | ListResourceTemplatesResult - | ListResourcesResult - | ReadResourceResult - | CallToolResult - | ListToolsResult - | InputRequiredResult; diff --git a/packages/core/src/types/specTypeSchema.examples.ts b/packages/core/src/types/specTypeSchema.examples.ts deleted file mode 100644 index c05f65e62d..0000000000 --- a/packages/core/src/types/specTypeSchema.examples.ts +++ /dev/null @@ -1,40 +0,0 @@ -/** - * Type-checked examples for `specTypeSchema.ts`. - * - * These examples are synced into JSDoc comments via the sync-snippets script. - * Each function's region markers define the code snippet that appears in the docs. - * - * @module - */ - -import { isSpecType, specTypeSchemas } from './specTypeSchema'; - -declare const untrusted: unknown; -declare const value: unknown; -declare const mixed: unknown[]; - -function specTypeSchemas_basicUsage() { - //#region specTypeSchemas_basicUsage - const result = specTypeSchemas.CallToolResult['~standard'].validate(untrusted); - if (result.issues === undefined) { - // result.value is CallToolResult - } - //#endregion specTypeSchemas_basicUsage - void result; -} - -function isSpecType_basicUsage() { - /* eslint-disable unicorn/no-array-callback-reference -- showcasing the guard-as-callback pattern */ - //#region isSpecType_basicUsage - if (isSpecType.ContentBlock(value)) { - // value is ContentBlock - } - - const blocks = mixed.filter(isSpecType.ContentBlock); - //#endregion isSpecType_basicUsage - /* eslint-enable unicorn/no-array-callback-reference */ - void blocks; -} - -void specTypeSchemas_basicUsage; -void isSpecType_basicUsage; diff --git a/packages/core/src/types/specTypeSchema.ts b/packages/core/src/types/specTypeSchema.ts deleted file mode 100644 index 95aa718d5c..0000000000 --- a/packages/core/src/types/specTypeSchema.ts +++ /dev/null @@ -1,301 +0,0 @@ -import type * as z from 'zod/v4'; - -import { - IdJagTokenExchangeResponseSchema, - OAuthClientInformationFullSchema, - OAuthClientInformationSchema, - OAuthClientMetadataSchema, - OAuthClientRegistrationErrorSchema, - OAuthErrorResponseSchema, - OAuthMetadataSchema, - OAuthProtectedResourceMetadataSchema, - OAuthTokenRevocationRequestSchema, - OAuthTokensSchema, - OpenIdProviderDiscoveryMetadataSchema, - OpenIdProviderMetadataSchema -} from '../shared/auth'; -import type { StandardSchemaV1, StandardSchemaV1Sync } from '../util/standardSchema'; -import * as schemas from './schemas'; - -/** - * Explicit allowlist of protocol Zod schemas that correspond to a public spec type in `types.ts`. - * - * This intentionally excludes internal helper schemas exported from `schemas.ts` that have no - * matching public type (e.g. `ListChangedOptionsBaseSchema`, `BaseRequestParamsSchema`, - * `NotificationsParamsSchema`, `ClientTasksCapabilitySchema`, `ServerTasksCapabilitySchema`). - * Keeping the list explicit means new public spec types must be added here deliberately, and - * internals never leak into `SpecTypeName`. - * - * `ResourceTemplateSchema` is included; its public type is exported as `ResourceTemplateType` - * (the bare name collides with the server package's `ResourceTemplate` class), so - * `SpecTypes['ResourceTemplate']` is structurally equal to `ResourceTemplateType` rather than to - * a type literally named `ResourceTemplate`. - */ -const SPEC_SCHEMA_KEYS = [ - 'AnnotationsSchema', - 'AudioContentSchema', - 'BaseMetadataSchema', - 'BlobResourceContentsSchema', - 'BooleanSchemaSchema', - 'CallToolRequestSchema', - 'CallToolRequestParamsSchema', - 'CallToolResultSchema', - 'CancelledNotificationSchema', - 'CancelledNotificationParamsSchema', - 'CancelTaskRequestSchema', - 'CancelTaskResultSchema', - 'ClientCapabilitiesSchema', - 'ClientNotificationSchema', - 'ClientRequestSchema', - 'ClientResultSchema', - 'CompatibilityCallToolResultSchema', - 'CompleteRequestSchema', - 'CompleteRequestParamsSchema', - 'CompleteResultSchema', - 'ContentBlockSchema', - 'CreateMessageRequestSchema', - 'CreateMessageRequestParamsSchema', - 'CreateMessageResultSchema', - 'CreateMessageResultWithToolsSchema', - 'CreateTaskResultSchema', - 'CursorSchema', - 'DiscoverRequestSchema', - 'DiscoverResultSchema', - 'ElicitationCompleteNotificationSchema', - 'ElicitationCompleteNotificationParamsSchema', - 'ElicitRequestSchema', - 'ElicitRequestFormParamsSchema', - 'ElicitRequestParamsSchema', - 'ElicitRequestURLParamsSchema', - 'ElicitResultSchema', - 'EmbeddedResourceSchema', - 'EmptyResultSchema', - 'EnumSchemaSchema', - 'GetPromptRequestSchema', - 'GetPromptRequestParamsSchema', - 'GetPromptResultSchema', - 'GetTaskPayloadRequestSchema', - 'GetTaskPayloadResultSchema', - 'GetTaskRequestSchema', - 'GetTaskResultSchema', - 'IconSchema', - 'IconsSchema', - 'ImageContentSchema', - 'ImplementationSchema', - 'InitializedNotificationSchema', - 'InitializeRequestSchema', - 'InitializeRequestParamsSchema', - 'InitializeResultSchema', - 'JSONArraySchema', - 'JSONObjectSchema', - 'JSONRPCErrorResponseSchema', - 'JSONRPCMessageSchema', - 'JSONRPCNotificationSchema', - 'JSONRPCRequestSchema', - 'JSONRPCResponseSchema', - 'JSONRPCResultResponseSchema', - 'JSONValueSchema', - 'LegacyTitledEnumSchemaSchema', - 'ListPromptsRequestSchema', - 'ListPromptsResultSchema', - 'ListResourcesRequestSchema', - 'ListResourcesResultSchema', - 'ListResourceTemplatesRequestSchema', - 'ListResourceTemplatesResultSchema', - 'ListRootsRequestSchema', - 'ListRootsResultSchema', - 'ListTasksRequestSchema', - 'ListTasksResultSchema', - 'ListToolsRequestSchema', - 'ListToolsResultSchema', - 'LoggingLevelSchema', - 'LoggingMessageNotificationSchema', - 'LoggingMessageNotificationParamsSchema', - 'ModelHintSchema', - 'ModelPreferencesSchema', - 'MultiSelectEnumSchemaSchema', - 'NotificationSchema', - 'NumberSchemaSchema', - 'PaginatedRequestSchema', - 'PaginatedRequestParamsSchema', - 'PaginatedResultSchema', - 'PingRequestSchema', - 'PrimitiveSchemaDefinitionSchema', - 'ProgressSchema', - 'ProgressNotificationSchema', - 'ProgressNotificationParamsSchema', - 'ProgressTokenSchema', - 'PromptSchema', - 'PromptArgumentSchema', - 'PromptListChangedNotificationSchema', - 'PromptMessageSchema', - 'PromptReferenceSchema', - 'ReadResourceRequestSchema', - 'ReadResourceRequestParamsSchema', - 'ReadResourceResultSchema', - 'RelatedTaskMetadataSchema', - 'RequestSchema', - 'RequestIdSchema', - 'RequestMetaEnvelopeSchema', - 'RequestMetaSchema', - 'ResourceSchema', - 'ResourceContentsSchema', - 'ResourceLinkSchema', - 'ResourceListChangedNotificationSchema', - 'ResourceRequestParamsSchema', - 'ResourceTemplateSchema', - 'ResourceTemplateReferenceSchema', - 'ResourceUpdatedNotificationSchema', - 'ResourceUpdatedNotificationParamsSchema', - 'ResultSchema', - 'RoleSchema', - 'RootSchema', - 'RootsListChangedNotificationSchema', - 'SamplingContentSchema', - 'SamplingMessageSchema', - 'SamplingMessageContentBlockSchema', - 'ServerCapabilitiesSchema', - 'ServerNotificationSchema', - 'ServerRequestSchema', - 'ServerResultSchema', - 'SetLevelRequestSchema', - 'SetLevelRequestParamsSchema', - 'SingleSelectEnumSchemaSchema', - 'StringSchemaSchema', - 'SubscribeRequestSchema', - 'SubscribeRequestParamsSchema', - 'TaskSchema', - 'TaskAugmentedRequestParamsSchema', - 'TaskCreationParamsSchema', - 'TaskMetadataSchema', - 'TaskStatusSchema', - 'TaskStatusNotificationSchema', - 'TaskStatusNotificationParamsSchema', - 'TextContentSchema', - 'TextResourceContentsSchema', - 'TitledMultiSelectEnumSchemaSchema', - 'TitledSingleSelectEnumSchemaSchema', - 'ToolSchema', - 'ToolAnnotationsSchema', - 'ToolChoiceSchema', - 'ToolExecutionSchema', - 'ToolListChangedNotificationSchema', - 'ToolResultContentSchema', - 'ToolUseContentSchema', - 'UnsubscribeRequestSchema', - 'UnsubscribeRequestParamsSchema', - 'UntitledMultiSelectEnumSchemaSchema', - 'UntitledSingleSelectEnumSchemaSchema' -] as const satisfies readonly (keyof typeof schemas)[]; - -const authSchemas = { - IdJagTokenExchangeResponseSchema, - OAuthClientInformationFullSchema, - OAuthClientInformationSchema, - OAuthClientMetadataSchema, - OAuthClientRegistrationErrorSchema, - OAuthErrorResponseSchema, - OAuthMetadataSchema, - OAuthProtectedResourceMetadataSchema, - OAuthTokenRevocationRequestSchema, - OAuthTokensSchema, - OpenIdProviderDiscoveryMetadataSchema, - OpenIdProviderMetadataSchema -} as const; - -type ProtocolSchemaKey = (typeof SPEC_SCHEMA_KEYS)[number]; -type AuthSchemaKey = keyof typeof authSchemas; -type SchemaKey = ProtocolSchemaKey | AuthSchemaKey; - -type SchemaFor = K extends ProtocolSchemaKey - ? (typeof schemas)[K] - : K extends AuthSchemaKey - ? (typeof authSchemas)[K] - : never; - -type StripSchemaSuffix = K extends `${infer N}Schema` ? N : never; - -/** - * Union of every named type in the SDK's protocol and OAuth schemas (e.g. `'CallToolResult'`, - * `'ContentBlock'`, `'Tool'`, `'OAuthTokens'`). Derived from the internal Zod schemas, so it stays - * in sync with the spec. - */ -export type SpecTypeName = StripSchemaSuffix; - -/** - * Maps each {@linkcode SpecTypeName} to its TypeScript type. - * - * `SpecTypes['CallToolResult']` is equivalent to importing the `CallToolResult` type directly. - */ -export type SpecTypes = { - [K in SchemaKey as StripSchemaSuffix]: SchemaFor extends z.ZodType ? z.output> : never; -}; - -/** - * Input shape for each {@linkcode SpecTypeName}. For most types this equals {@linkcode SpecTypes}, - * but a few schemas apply defaults/preprocessing, so the accepted input may be looser than the - * resulting output type. - */ -type SpecTypeInputs = { - [K in SchemaKey as StripSchemaSuffix]: SchemaFor extends z.ZodType ? z.input> : never; -}; - -type SchemaRecord = { readonly [K in SpecTypeName]: StandardSchemaV1Sync }; -type GuardRecord = { readonly [K in SpecTypeName]: (value: unknown) => value is SpecTypeInputs[K] }; - -const _specTypeSchemas: Record = {}; -const _isSpecType: Record boolean> = {}; -function register(key: string, schema: z.ZodType): void { - const name = key.slice(0, -'Schema'.length); - _specTypeSchemas[name] = schema; - _isSpecType[name] = (v: unknown) => schema.safeParse(v).success; -} -for (const key of SPEC_SCHEMA_KEYS) { - // eslint-disable-next-line import/namespace -- key is constrained to keyof typeof schemas via the satisfies clause above - register(key, schemas[key]); -} -for (const [key, schema] of Object.entries(authSchemas)) { - register(key, schema); -} - -/** - * Runtime validators for every MCP spec type, keyed by type name. - * - * Use this when you need to validate a spec-defined shape at a boundary the SDK does not own, for - * example an extension's custom-method payload that embeds a `CallToolResult`, or a value read from - * storage that should be a `Tool`. - * - * Each entry implements the Standard Schema interface, so it composes with any - * Standard-Schema-aware library. For a simple boolean check, use {@linkcode isSpecType} instead. - * - * @example - * ```ts source="./specTypeSchema.examples.ts#specTypeSchemas_basicUsage" - * const result = specTypeSchemas.CallToolResult['~standard'].validate(untrusted); - * if (result.issues === undefined) { - * // result.value is CallToolResult - * } - * ``` - */ -export const specTypeSchemas: SchemaRecord = Object.freeze(_specTypeSchemas as SchemaRecord); - -/** - * Type predicates for every MCP spec type, keyed by type name. - * - * Returns `true` if the value satisfies the schema's input type (`z.input<>`, before defaults and - * transforms are applied), and narrows to that input type. For schemas with `.default()` or - * `.preprocess()`, this may accept values that do not structurally match the named output type; - * for example `isSpecType.CallToolResult({})` is `true` because `content` has a default. Use - * `specTypeSchemas.X['~standard'].validate(value)` when you need the validated output value. - * - * Each guard is a standalone function, so it can be passed directly as a callback. - * - * @example - * ```ts source="./specTypeSchema.examples.ts#isSpecType_basicUsage" - * if (isSpecType.ContentBlock(value)) { - * // value is ContentBlock - * } - * - * const blocks = mixed.filter(isSpecType.ContentBlock); - * ``` - */ -export const isSpecType: GuardRecord = Object.freeze(_isSpecType as GuardRecord); diff --git a/packages/core/src/types/types.ts b/packages/core/src/types/types.ts deleted file mode 100644 index 9abc68f79e..0000000000 --- a/packages/core/src/types/types.ts +++ /dev/null @@ -1,604 +0,0 @@ -// ⚠️ PUBLIC API — every export from this file is re-exported via `export *` -// in exports/public/index.ts and becomes part of the SDK's public surface. -// Only add MCP-spec-derived types here. Internal helpers belong elsewhere. - -import type * as z from 'zod/v4'; - -import type { INTERNAL_ERROR, INVALID_PARAMS, INVALID_REQUEST, METHOD_NOT_FOUND, PARSE_ERROR } from './constants'; -import type { - AnnotationsSchema, - AudioContentSchema, - BaseMetadataSchema, - BaseRequestParamsSchema, - BlobResourceContentsSchema, - BooleanSchemaSchema, - CallToolRequestParamsSchema, - CallToolRequestSchema, - CallToolResultSchema, - CancelledNotificationParamsSchema, - CancelledNotificationSchema, - CancelTaskRequestSchema, - CancelTaskResultSchema, - ClientCapabilitiesSchema, - ClientNotificationSchema, - ClientRequestSchema, - ClientResultSchema, - CompatibilityCallToolResultSchema, - CompleteRequestParamsSchema, - CompleteRequestSchema, - CompleteResultSchema, - ContentBlockSchema, - CreateMessageRequestParamsSchema, - CreateMessageRequestSchema, - CreateMessageResultSchema, - CreateMessageResultWithToolsSchema, - CreateTaskResultSchema, - CursorSchema, - DiscoverRequestSchema, - DiscoverResultSchema, - ElicitationCompleteNotificationParamsSchema, - ElicitationCompleteNotificationSchema, - ElicitRequestFormParamsSchema, - ElicitRequestParamsSchema, - ElicitRequestSchema, - ElicitRequestURLParamsSchema, - ElicitResultSchema, - EmbeddedResourceSchema, - EmptyResultSchema, - EnumSchemaSchema, - GetPromptRequestParamsSchema, - GetPromptRequestSchema, - GetPromptResultSchema, - GetTaskPayloadRequestSchema, - GetTaskPayloadResultSchema, - GetTaskRequestSchema, - GetTaskResultSchema, - IconSchema, - IconsSchema, - ImageContentSchema, - ImplementationSchema, - InitializedNotificationSchema, - InitializeRequestParamsSchema, - InitializeRequestSchema, - InitializeResultSchema, - JSONRPCErrorResponseSchema, - JSONRPCMessageSchema, - JSONRPCNotificationSchema, - JSONRPCRequestSchema, - JSONRPCResponseSchema, - JSONRPCResultResponseSchema, - LegacyTitledEnumSchemaSchema, - ListPromptsRequestSchema, - ListPromptsResultSchema, - ListResourcesRequestSchema, - ListResourcesResultSchema, - ListResourceTemplatesRequestSchema, - ListResourceTemplatesResultSchema, - ListRootsRequestSchema, - ListRootsResultSchema, - ListTasksRequestSchema, - ListTasksResultSchema, - ListToolsRequestSchema, - ListToolsResultSchema, - LoggingLevelSchema, - LoggingMessageNotificationParamsSchema, - LoggingMessageNotificationSchema, - ModelHintSchema, - ModelPreferencesSchema, - MultiSelectEnumSchemaSchema, - NotificationSchema, - NotificationsParamsSchema, - NumberSchemaSchema, - PaginatedRequestParamsSchema, - PaginatedRequestSchema, - PaginatedResultSchema, - PingRequestSchema, - PrimitiveSchemaDefinitionSchema, - ProgressNotificationParamsSchema, - ProgressNotificationSchema, - ProgressSchema, - ProgressTokenSchema, - PromptArgumentSchema, - PromptListChangedNotificationSchema, - PromptMessageSchema, - PromptReferenceSchema, - PromptSchema, - ReadResourceRequestParamsSchema, - ReadResourceRequestSchema, - ReadResourceResultSchema, - RelatedTaskMetadataSchema, - RequestIdSchema, - RequestMetaEnvelopeSchema, - RequestMetaSchema, - RequestSchema, - ResourceContentsSchema, - ResourceLinkSchema, - ResourceListChangedNotificationSchema, - ResourceRequestParamsSchema, - ResourceSchema, - ResourceTemplateReferenceSchema, - ResourceTemplateSchema, - ResourceUpdatedNotificationParamsSchema, - ResourceUpdatedNotificationSchema, - ResultSchema, - RoleSchema, - RootSchema, - RootsListChangedNotificationSchema, - SamplingContentSchema, - SamplingMessageContentBlockSchema, - SamplingMessageSchema, - ServerCapabilitiesSchema, - ServerNotificationSchema, - ServerRequestSchema, - ServerResultSchema, - SetLevelRequestParamsSchema, - SetLevelRequestSchema, - SingleSelectEnumSchemaSchema, - StringSchemaSchema, - SubscribeRequestParamsSchema, - SubscribeRequestSchema, - TaskAugmentedRequestParamsSchema, - TaskCreationParamsSchema, - TaskMetadataSchema, - TaskSchema, - TaskStatusNotificationParamsSchema, - TaskStatusNotificationSchema, - TaskStatusSchema, - TextContentSchema, - TextResourceContentsSchema, - TitledMultiSelectEnumSchemaSchema, - TitledSingleSelectEnumSchemaSchema, - ToolAnnotationsSchema, - ToolChoiceSchema, - ToolExecutionSchema, - ToolListChangedNotificationSchema, - ToolResultContentSchema, - ToolSchema, - ToolUseContentSchema, - UnsubscribeRequestParamsSchema, - UnsubscribeRequestSchema, - UntitledMultiSelectEnumSchemaSchema, - UntitledSingleSelectEnumSchemaSchema -} from './schemas'; - -/* JSON types */ -export type JSONValue = string | number | boolean | null | JSONObject | JSONArray; -export type JSONObject = { [key: string]: JSONValue }; -export type JSONArray = JSONValue[]; - -/** - * Utility types - */ -type ExpandRecursively = T extends object ? (T extends infer O ? { [K in keyof O]: ExpandRecursively } : never) : T; - -type Primitive = string | number | boolean | bigint | null | undefined; -type Flatten = T extends Primitive - ? T - : T extends Array - ? Array> - : T extends Set - ? Set> - : T extends Map - ? Map, Flatten> - : T extends object - ? { [K in keyof T]: Flatten } - : T; - -type Infer = Flatten>; - -/* JSON-RPC types */ -export type ProgressToken = Infer; -export type Cursor = Infer; -export type Request = Infer; -export type TaskAugmentedRequestParams = Infer; -export type RequestMeta = Infer; -export type Notification = Infer; -export type Result = Infer; -export type RequestId = Infer; -export type JSONRPCRequest = Infer; -export type JSONRPCNotification = Infer; -export type JSONRPCResponse = Infer; -export type JSONRPCErrorResponse = Infer; -export type JSONRPCResultResponse = Infer; -export type JSONRPCMessage = Infer; -export type RequestParams = Infer; -export type NotificationParams = Infer; -/** - * The per-request `_meta` envelope carried by every request under protocol revision - * 2026-07-28 (protocol version, client info, client capabilities, optional log level). - */ -export type RequestMetaEnvelope = Infer; - -/* Empty result */ -export type EmptyResult = Infer; - -/* Cancellation */ -export type CancelledNotificationParams = Infer; -export type CancelledNotification = Infer; - -/* Base Metadata */ -export type Icon = Infer; -export type Icons = Infer; -export type BaseMetadata = Infer; -export type Annotations = Infer; -export type Role = Infer; - -/* Initialization */ -export type Implementation = Infer; -/** - * Capabilities a client may support. - * - * Note: the `roots` and `sampling` capabilities are deprecated as of protocol - * version 2026-07-28 (SEP-2577); they remain in the specification for at least - * twelve months. See `ClientCapabilitiesSchema`. - */ -export type ClientCapabilities = Infer; -export type InitializeRequestParams = Infer; -export type InitializeRequest = Infer; -/** - * Capabilities a server may support. - * - * Note: the `logging` capability is deprecated as of protocol version - * 2026-07-28 (SEP-2577); it remains in the specification for at least twelve - * months. See `ServerCapabilitiesSchema`. - */ -export type ServerCapabilities = Infer; -export type InitializeResult = Infer; -export type InitializedNotification = Infer; - -/* Discovery */ -export type DiscoverRequest = Infer; -export type DiscoverResult = Infer; - -/* Ping */ -export type PingRequest = Infer; - -/* Progress notifications */ -export type Progress = Infer; -export type ProgressNotificationParams = Infer; -export type ProgressNotification = Infer; - -/* Tasks */ -export type Task = Infer; -export type TaskStatus = Infer; -export type TaskCreationParams = Infer; -export type TaskMetadata = Infer; -export type RelatedTaskMetadata = Infer; -export type CreateTaskResult = Infer; -export type TaskStatusNotificationParams = Infer; -export type TaskStatusNotification = Infer; -export type GetTaskRequest = Infer; -export type GetTaskResult = Infer; -export type GetTaskPayloadRequest = Infer; -export type ListTasksRequest = Infer; -export type ListTasksResult = Infer; -export type CancelTaskRequest = Infer; -export type CancelTaskResult = Infer; -export type GetTaskPayloadResult = Infer; - -/* Pagination */ -export type PaginatedRequestParams = Infer; -export type PaginatedRequest = Infer; -export type PaginatedResult = Infer; - -/* Resources */ -export type ResourceContents = Infer; -export type TextResourceContents = Infer; -export type BlobResourceContents = Infer; -export type Resource = Infer; -// TODO: Overlaps with exported `ResourceTemplate` class from `server`. -export type ResourceTemplateType = Infer; -export type ListResourcesRequest = Infer; -export type ListResourcesResult = Infer; -export type ListResourceTemplatesRequest = Infer; -export type ListResourceTemplatesResult = Infer; -export type ResourceRequestParams = Infer; -export type ReadResourceRequestParams = Infer; -export type ReadResourceRequest = Infer; -export type ReadResourceResult = Infer; -export type ResourceListChangedNotification = Infer; -export type SubscribeRequestParams = Infer; -export type SubscribeRequest = Infer; -export type UnsubscribeRequestParams = Infer; -export type UnsubscribeRequest = Infer; -export type ResourceUpdatedNotificationParams = Infer; -export type ResourceUpdatedNotification = Infer; - -/* Prompts */ -export type PromptArgument = Infer; -export type Prompt = Infer; -export type ListPromptsRequest = Infer; -export type ListPromptsResult = Infer; -export type GetPromptRequestParams = Infer; -export type GetPromptRequest = Infer; -export type TextContent = Infer; -export type ImageContent = Infer; -export type AudioContent = Infer; -export type ToolUseContent = Infer; -export type ToolResultContent = Infer; -export type EmbeddedResource = Infer; -export type ResourceLink = Infer; -export type ContentBlock = Infer; -export type PromptMessage = Infer; -export type GetPromptResult = Infer; -export type PromptListChangedNotification = Infer; - -/* Tools */ -export type ToolAnnotations = Infer; -export type ToolExecution = Infer; -export type Tool = Infer; -export type ListToolsRequest = Infer; -export type ListToolsResult = Infer; -export type CallToolRequestParams = Infer; -export type CallToolResult = Infer; -export type CompatibilityCallToolResult = Infer; -export type CallToolRequest = Infer; -export type ToolListChangedNotification = Infer; - -/* Logging */ -export type LoggingLevel = Infer; -export type SetLevelRequestParams = Infer; -export type SetLevelRequest = Infer; -export type LoggingMessageNotificationParams = Infer; -export type LoggingMessageNotification = Infer; - -/* Sampling */ -export type ToolChoice = Infer; -export type ModelHint = Infer; -export type ModelPreferences = Infer; -export type SamplingContent = Infer; -export type SamplingMessageContentBlock = Infer; -export type SamplingMessage = Infer; -export type CreateMessageRequestParams = Infer; -export type CreateMessageRequest = Infer; -export type CreateMessageResult = Infer; -export type CreateMessageResultWithTools = Infer; - -/* Elicitation */ -export type BooleanSchema = Infer; -export type StringSchema = Infer; -export type NumberSchema = Infer; -export type EnumSchema = Infer; -export type UntitledSingleSelectEnumSchema = Infer; -export type TitledSingleSelectEnumSchema = Infer; -export type LegacyTitledEnumSchema = Infer; -export type UntitledMultiSelectEnumSchema = Infer; -export type TitledMultiSelectEnumSchema = Infer; -export type SingleSelectEnumSchema = Infer; -export type MultiSelectEnumSchema = Infer; -export type PrimitiveSchemaDefinition = Infer; -export type ElicitRequestParams = Infer; -export type ElicitRequestFormParams = Infer; -export type ElicitRequestURLParams = Infer; -export type ElicitRequest = Infer; -export type ElicitationCompleteNotificationParams = Infer; -export type ElicitationCompleteNotification = Infer; -export type ElicitResult = Infer; - -/* Autocomplete */ -export type ResourceTemplateReference = Infer; -export type PromptReference = Infer; -export type CompleteRequestParams = Infer; -export type CompleteRequest = Infer; -export type CompleteResult = Infer; - -/* Roots */ -export type Root = Infer; -export type ListRootsRequest = Infer; -export type ListRootsResult = Infer; -export type RootsListChangedNotification = Infer; - -/* Client messages */ -export type ClientRequest = Infer; -export type ClientNotification = Infer; -export type ClientResult = Infer; - -/* Server messages */ -export type ServerRequest = Infer; -export type ServerNotification = Infer; -export type ServerResult = Infer; - -/* Protocol type maps */ -type MethodToTypeMap = { - [T in U as T extends { method: infer M extends string } ? M : never]: T; -}; -export type RequestMethod = ClientRequest['method'] | ServerRequest['method']; -export type NotificationMethod = ClientNotification['method'] | ServerNotification['method']; -export type RequestTypeMap = MethodToTypeMap; -export type NotificationTypeMap = MethodToTypeMap; -export type ResultTypeMap = { - ping: EmptyResult; - initialize: InitializeResult; - 'completion/complete': CompleteResult; - 'logging/setLevel': EmptyResult; - 'prompts/get': GetPromptResult; - 'prompts/list': ListPromptsResult; - 'resources/list': ListResourcesResult; - 'resources/templates/list': ListResourceTemplatesResult; - 'resources/read': ReadResourceResult; - 'resources/subscribe': EmptyResult; - 'resources/unsubscribe': EmptyResult; - 'tools/call': CallToolResult | CreateTaskResult; - 'tools/list': ListToolsResult; - 'sampling/createMessage': CreateMessageResult | CreateMessageResultWithTools | CreateTaskResult; - 'elicitation/create': ElicitResult | CreateTaskResult; - 'roots/list': ListRootsResult; - 'tasks/get': GetTaskResult; - 'tasks/result': Result; - 'tasks/list': ListTasksResult; - 'tasks/cancel': CancelTaskResult; -}; - -/** - * Information about a validated access token, provided to request handlers. - */ -export interface AuthInfo { - /** - * The access token. - */ - token: string; - - /** - * The client ID associated with this token. - */ - clientId: string; - - /** - * Scopes associated with this token. - */ - scopes: string[]; - - /** - * When the token expires (in seconds since epoch). - */ - expiresAt?: number; - - /** - * The RFC 8707 resource server identifier for which this token is valid. - * If set, this MUST match the MCP server's resource identifier (minus hash fragment). - */ - resource?: URL; - - /** - * Additional data associated with the token. - * This field should be used for any additional data that needs to be attached to the auth info. - */ - extra?: Record; -} - -type JSONRPCErrorObject = { code: number; message: string; data?: unknown }; - -export interface ParseError extends JSONRPCErrorObject { - code: typeof PARSE_ERROR; -} -export interface InvalidRequestError extends JSONRPCErrorObject { - code: typeof INVALID_REQUEST; -} -export interface MethodNotFoundError extends JSONRPCErrorObject { - code: typeof METHOD_NOT_FOUND; -} -export interface InvalidParamsError extends JSONRPCErrorObject { - code: typeof INVALID_PARAMS; -} -export interface InternalError extends JSONRPCErrorObject { - code: typeof INTERNAL_ERROR; -} - -/** - * Data carried by a `-32004` UnsupportedProtocolVersion protocol error - * (protocol revision 2026-07-28). - */ -export interface UnsupportedProtocolVersionErrorData { - /** - * Protocol versions the receiver supports. The sender should choose a - * mutually supported version from this list and retry. - */ - supported: string[]; - /** - * The protocol version that was requested. - */ - requested: string; -} - -/** - * Callback type for list changed notifications. - */ -export type ListChangedCallback = (error: Error | null, items: T[] | null) => void; - -/** - * Options for subscribing to list changed notifications. - * - * @typeParam T - The type of items in the list (`Tool`, `Prompt`, or `Resource`) - */ -export type ListChangedOptions = { - /** - * If `true`, the list will be refreshed automatically when a list changed notification is received. - * @default true - */ - autoRefresh?: boolean; - /** - * Debounce time in milliseconds. Set to `0` to disable. - * @default 300 - */ - debounceMs?: number; - /** - * Callback invoked when the list changes. - * - * If `autoRefresh` is `true`, `items` contains the updated list. - * If `autoRefresh` is `false`, `items` is `null` (caller should refresh manually). - */ - onChanged: ListChangedCallback; -}; - -/** - * Configuration for list changed notification handlers. - * - * Use this to configure handlers for tools, prompts, and resources list changes - * when creating a client. - * - * Note: Handlers are only activated if the server advertises the corresponding - * `listChanged` capability (e.g., `tools.listChanged: true`). If the server - * doesn't advertise this capability, the handler will not be set up. - */ -export type ListChangedHandlers = { - /** - * Handler for tool list changes. - */ - tools?: ListChangedOptions; - /** - * Handler for prompt list changes. - */ - prompts?: ListChangedOptions; - /** - * Handler for resource list changes. - */ - resources?: ListChangedOptions; -}; - -/** - * Extra information about a message. - */ -export interface MessageExtraInfo { - /** - * The original HTTP request. - */ - request?: globalThis.Request; - - /** - * The authentication information. - */ - authInfo?: AuthInfo; - - /** - * Callback to close the SSE stream for this request, triggering client reconnection. - * Only available when using {@linkcode @modelcontextprotocol/node!streamableHttp.NodeStreamableHTTPServerTransport | NodeStreamableHTTPServerTransport} with eventStore configured. - */ - closeSSEStream?: () => void; - - /** - * Callback to close the standalone GET SSE stream, triggering client reconnection. - * Only available when using {@linkcode @modelcontextprotocol/node!streamableHttp.NodeStreamableHTTPServerTransport | NodeStreamableHTTPServerTransport} with eventStore configured. - */ - closeStandaloneSSEStream?: () => void; -} - -export type MetaObject = Record; -export type RequestMetaObject = RequestMeta; - -/** - * {@linkcode CreateMessageRequestParams} without tools - for backwards-compatible overload. - * Excludes tools/toolChoice to indicate they should not be provided. - */ -export type CreateMessageRequestParamsBase = Omit; - -/** - * {@linkcode CreateMessageRequestParams} with required tools - for tool-enabled overload. - */ -export interface CreateMessageRequestParamsWithTools extends CreateMessageRequestParams { - tools: Tool[]; -} - -export type CompleteRequestResourceTemplate = ExpandRecursively< - CompleteRequest & { params: CompleteRequestParams & { ref: ResourceTemplateReference } } ->; -export type CompleteRequestPrompt = ExpandRecursively; diff --git a/packages/core/src/util/inMemory.ts b/packages/core/src/util/inMemory.ts deleted file mode 100644 index 3afd2b1ac7..0000000000 --- a/packages/core/src/util/inMemory.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { SdkError, SdkErrorCode } from '../errors/sdkErrors'; -import type { Transport } from '../shared/transport'; -import type { AuthInfo, JSONRPCMessage, RequestId } from '../types/index'; - -interface QueuedMessage { - message: JSONRPCMessage; - extra?: { authInfo?: AuthInfo }; -} - -/** - * In-memory transport for creating clients and servers that talk to each other within the same process. - * - * Intended for testing and development. For production in-process connections, use - * `StreamableHTTPClientTransport` against a local server URL. - */ -export class InMemoryTransport implements Transport { - private _otherTransport?: InMemoryTransport; - private _messageQueue: QueuedMessage[] = []; - private _closed = false; - - onclose?: () => void; - onerror?: (error: Error) => void; - onmessage?: (message: JSONRPCMessage, extra?: { authInfo?: AuthInfo }) => void; - sessionId?: string; - - /** - * Creates a pair of linked in-memory transports that can communicate with each other. One should be passed to a {@linkcode @modelcontextprotocol/client!client/client.Client | Client} and one to a {@linkcode @modelcontextprotocol/server!server/server.Server | Server}. - */ - static createLinkedPair(): [InMemoryTransport, InMemoryTransport] { - const clientTransport = new InMemoryTransport(); - const serverTransport = new InMemoryTransport(); - clientTransport._otherTransport = serverTransport; - serverTransport._otherTransport = clientTransport; - return [clientTransport, serverTransport]; - } - - async start(): Promise { - // Process any messages that were queued before start was called - while (this._messageQueue.length > 0) { - const queuedMessage = this._messageQueue.shift()!; - this.onmessage?.(queuedMessage.message, queuedMessage.extra); - } - } - - async close(): Promise { - if (this._closed) return; - this._closed = true; - - const other = this._otherTransport; - this._otherTransport = undefined; - try { - await other?.close(); - } finally { - this.onclose?.(); - } - } - - /** - * Sends a message with optional auth info. - * This is useful for testing authentication scenarios. - */ - async send(message: JSONRPCMessage, options?: { relatedRequestId?: RequestId; authInfo?: AuthInfo }): Promise { - if (!this._otherTransport) { - throw new SdkError(SdkErrorCode.NotConnected, 'Not connected'); - } - - if (this._otherTransport.onmessage) { - this._otherTransport.onmessage(message, { authInfo: options?.authInfo }); - } else { - this._otherTransport._messageQueue.push({ message, extra: { authInfo: options?.authInfo } }); - } - } -} diff --git a/packages/core/src/util/schema.ts b/packages/core/src/util/schema.ts deleted file mode 100644 index 9676674b84..0000000000 --- a/packages/core/src/util/schema.ts +++ /dev/null @@ -1,32 +0,0 @@ -/** - * Internal Zod schema utilities for protocol handling. - * These are used internally by the SDK for protocol message validation. - */ - -import * as z from 'zod/v4'; - -/** - * Base type for any Zod schema. - */ -export type AnySchema = z.core.$ZodType; - -/** - * A Zod schema for objects specifically. - */ -export type AnyObjectSchema = z.core.$ZodObject; - -/** - * Extracts the output type from a Zod schema. - */ -export type SchemaOutput = z.output; - -/** - * Parses data against a Zod schema (synchronous). - * Returns a discriminated union with success/error. - */ -export function parseSchema( - schema: T, - data: unknown -): { success: true; data: z.output } | { success: false; error: z.core.$ZodError } { - return z.safeParse(schema, data); -} diff --git a/packages/core/src/util/standardSchema.ts b/packages/core/src/util/standardSchema.ts deleted file mode 100644 index b938885de0..0000000000 --- a/packages/core/src/util/standardSchema.ts +++ /dev/null @@ -1,251 +0,0 @@ -/** - * Standard Schema utilities for user-provided schemas. - * Supports Zod v4, Valibot, ArkType, and other Standard Schema implementations. - * @see https://standardschema.dev - */ - -/* eslint-disable @typescript-eslint/no-namespace */ - -import * as z from 'zod/v4'; - -// Standard Schema interfaces — vendored from https://standardschema.dev (spec v1, Jan 2025) - -export interface StandardTypedV1 { - readonly '~standard': StandardTypedV1.Props; -} - -export namespace StandardTypedV1 { - export interface Props { - readonly version: 1; - readonly vendor: string; - readonly types?: Types | undefined; - } - - export interface Types { - readonly input: Input; - readonly output: Output; - } - - export type InferInput = NonNullable['input']; - export type InferOutput = NonNullable['output']; -} - -export interface StandardSchemaV1 { - readonly '~standard': StandardSchemaV1.Props; -} - -export namespace StandardSchemaV1 { - export interface Props extends StandardTypedV1.Props { - readonly validate: (value: unknown, options?: Options | undefined) => Result | Promise>; - } - - export interface Options { - readonly libraryOptions?: Record | undefined; - } - - export type Result = SuccessResult | FailureResult; - - export interface SuccessResult { - readonly value: Output; - readonly issues?: undefined; - } - - export interface FailureResult { - readonly issues: ReadonlyArray; - } - - export interface Issue { - readonly message: string; - readonly path?: ReadonlyArray | undefined; - } - - export interface PathSegment { - readonly key: PropertyKey; - } - - export type InferInput = StandardTypedV1.InferInput; - export type InferOutput = StandardTypedV1.InferOutput; -} - -export interface StandardJSONSchemaV1 { - readonly '~standard': StandardJSONSchemaV1.Props; -} - -export namespace StandardJSONSchemaV1 { - export interface Props extends StandardTypedV1.Props { - readonly jsonSchema: Converter; - } - - export interface Converter { - readonly input: (options: Options) => Record; - readonly output: (options: Options) => Record; - } - - export type Target = 'draft-2020-12' | 'draft-07' | 'openapi-3.0' | (object & string); - - export interface Options { - readonly target: Target; - readonly libraryOptions?: Record | undefined; - } - - export type InferInput = StandardTypedV1.InferInput; - export type InferOutput = StandardTypedV1.InferOutput; -} - -/** - * Combined interface for schemas with both validation and JSON Schema conversion — - * the intersection of {@linkcode StandardSchemaV1} and {@linkcode StandardJSONSchemaV1}. - * - * This is the type accepted by `registerTool` / `registerPrompt`. The SDK needs - * `~standard.jsonSchema` to advertise the tool's argument shape in `tools/list`, and - * `~standard.validate` to check incoming arguments when a `tools/call` arrives. - * - * Zod v4, ArkType, and Valibot (via `@valibot/to-json-schema`'s `toStandardJsonSchema`) - * all implement both interfaces. - * - * @see https://standardschema.dev/ for the Standard Schema specification - */ -export interface StandardSchemaWithJSON { - readonly '~standard': StandardSchemaV1.Props & StandardJSONSchemaV1.Props; -} - -export namespace StandardSchemaWithJSON { - export type InferInput = StandardTypedV1.InferInput; - export type InferOutput = StandardTypedV1.InferOutput; -} - -/** - * Narrowing of {@linkcode StandardSchemaV1} whose `validate` is guaranteed synchronous. - * - * The Zod schemas backing `specTypeSchemas` contain no async refinements or transforms, - * so every entry satisfies this interface. Consumers can call `validate()` and access - * `.issues` / `.value` on the result without `await`. - * - * `StandardSchemaV1Sync` is assignable to `StandardSchemaV1` — it is a strict subtype. - */ -export interface StandardSchemaV1Sync extends StandardSchemaV1 { - readonly '~standard': StandardSchemaV1Sync.Props; -} - -export namespace StandardSchemaV1Sync { - export interface Props extends StandardSchemaV1.Props { - readonly validate: (value: unknown, options?: StandardSchemaV1.Options | undefined) => StandardSchemaV1.Result; - } - - export type InferInput = StandardTypedV1.InferInput; - export type InferOutput = StandardTypedV1.InferOutput; -} - -// Type guards - -export function isStandardJSONSchema(schema: unknown): schema is StandardJSONSchemaV1 { - if (schema == null) return false; - const schemaType = typeof schema; - if (schemaType !== 'object' && schemaType !== 'function') return false; - if (!('~standard' in (schema as object))) return false; - const std = (schema as StandardJSONSchemaV1)['~standard']; - return typeof std?.jsonSchema?.input === 'function' && typeof std?.jsonSchema?.output === 'function'; -} - -export function isStandardSchema(schema: unknown): schema is StandardSchemaV1 { - if (schema == null) return false; - const schemaType = typeof schema; - if (schemaType !== 'object' && schemaType !== 'function') return false; - if (!('~standard' in (schema as object))) return false; - const std = (schema as StandardSchemaV1)['~standard']; - return typeof std?.validate === 'function'; -} - -export function isStandardSchemaWithJSON(schema: unknown): schema is StandardSchemaWithJSON { - return isStandardJSONSchema(schema) && isStandardSchema(schema); -} - -// JSON Schema conversion - -let warnedZodFallback = false; - -/** - * Converts a StandardSchema to JSON Schema for use as an MCP tool/prompt schema. - * - * MCP requires `type: "object"` at the root of tool inputSchema/outputSchema and - * prompt argument schemas. Zod's discriminated unions emit `{oneOf: [...]}` without - * a top-level `type`, so this function defaults `type` to `"object"` when absent. - * - * Throws if the schema has an explicit non-object `type` (e.g. `z.string()`), - * since that cannot satisfy the MCP spec. - */ -export function standardSchemaToJsonSchema(schema: StandardJSONSchemaV1, io: 'input' | 'output' = 'input'): Record { - const std = schema['~standard']; - let result: Record; - if (std.jsonSchema) { - result = std.jsonSchema[io]({ target: 'draft-2020-12' }); - } else if (std.vendor === 'zod') { - // zod 4.0–4.1 implements StandardSchemaV1 but not StandardJSONSchemaV1 (`~standard.jsonSchema`). - // The SDK already bundles zod 4, so fall back to its converter rather than crashing on tools/list. - // zod 3 schemas (which also report vendor 'zod') have `_def` but not `_zod`; the SDK-bundled - // zod 4 `z.toJSONSchema()` cannot introspect them, so throw a clear error instead of crashing. - if (!('_zod' in (schema as object))) { - throw new Error( - 'Schema appears to be from zod 3, which the SDK cannot convert to JSON Schema. ' + - 'Upgrade to zod >=4.2.0, or wrap your JSON Schema with fromJsonSchema().' - ); - } - if (!warnedZodFallback) { - warnedZodFallback = true; - console.warn( - '[mcp-sdk] Your zod version does not implement `~standard.jsonSchema` (added in zod 4.2.0). ' + - 'Falling back to z.toJSONSchema(). Upgrade to zod >=4.2.0 to silence this warning.' - ); - } - result = z.toJSONSchema(schema as unknown as z.ZodType, { target: 'draft-2020-12', io }) as Record; - } else { - throw new Error( - `Schema library "${std.vendor}" does not implement StandardJSONSchemaV1 (\`~standard.jsonSchema\`). ` + - `Upgrade to a version that does, or wrap your JSON Schema with fromJsonSchema().` - ); - } - if (result.type !== undefined && result.type !== 'object') { - throw new Error( - `MCP tool and prompt schemas must describe objects (got type: ${JSON.stringify(result.type)}). ` + - `Wrap your schema in z.object({...}) or equivalent.` - ); - } - return { type: 'object', ...result }; -} - -// Validation - -export type StandardSchemaValidationResult = { success: true; data: T } | { success: false; error: string }; - -function formatIssue(issue: StandardSchemaV1.Issue): string { - if (!issue.path?.length) return issue.message; - const path = issue.path.map(p => String(typeof p === 'object' ? p.key : p)).join('.'); - return `${path}: ${issue.message}`; -} - -export async function validateStandardSchema( - schema: T, - data: unknown -): Promise>> { - const result = await schema['~standard'].validate(data); - if (result.issues && result.issues.length > 0) { - return { success: false, error: result.issues.map(i => formatIssue(i)).join(', ') }; - } - return { success: true, data: (result as StandardSchemaV1.SuccessResult).value as StandardSchemaV1.InferOutput }; -} - -// Prompt argument extraction - -export function promptArgumentsFromStandardSchema( - schema: StandardJSONSchemaV1 -): Array<{ name: string; description?: string; required: boolean }> { - const jsonSchema = standardSchemaToJsonSchema(schema, 'input'); - const properties = (jsonSchema.properties as Record) || {}; - const required = (jsonSchema.required as string[]) || []; - - return Object.entries(properties).map(([name, prop]) => ({ - name, - description: prop?.description, - required: required.includes(name) - })); -} diff --git a/packages/core/src/util/zodCompat.ts b/packages/core/src/util/zodCompat.ts deleted file mode 100644 index 249dba5154..0000000000 --- a/packages/core/src/util/zodCompat.ts +++ /dev/null @@ -1,80 +0,0 @@ -/** - * Zod-specific helpers for the v1-compat raw-shape shorthand on - * `registerTool`/`registerPrompt`. Kept separate from `standardSchema.ts` so - * that file stays library-agnostic per the Standard Schema spec. - */ - -import * as z from 'zod/v4'; - -import type { StandardSchemaWithJSON } from './standardSchema'; -import { isStandardSchema } from './standardSchema'; - -function isZodV4Schema(v: unknown): v is z.ZodType { - // `_zod` is the v4 internal namespace property. Zod v3 schemas have `_def` - // and (since 3.24) `~standard.vendor === 'zod'`, but never `_zod`. We require - // v4 because the wrap path below uses v4's `z.object()`, which cannot consume - // v3 field schemas. - return typeof v === 'object' && v !== null && '_zod' in v; -} - -function looksLikeZodV3(v: unknown): boolean { - // v3 schemas have `_def.typeName` (e.g. 'ZodString') and no `_zod`. - return ( - typeof v === 'object' && - v !== null && - !('_zod' in v) && - '_def' in v && - typeof (v as { _def?: { typeName?: unknown } })._def?.typeName === 'string' - ); -} - -/** - * Detects a "raw shape" — a plain object whose values are Zod field schemas, - * e.g. `{ name: z.string() }`. Powers the auto-wrap in - * {@linkcode normalizeRawShapeSchema}, which wraps with `z.object()`, so only - * Zod values are supported. - * - * @internal - */ -export function isZodRawShape(obj: unknown): obj is Record { - if (typeof obj !== 'object' || obj === null) return false; - if (isStandardSchema(obj)) return false; - // Require a plain object literal: rejects arrays, Date, Map, RegExp, class instances, etc. - // Object.create(null) is also accepted. - const proto = Object.getPrototypeOf(obj); - if (proto !== Object.prototype && proto !== null) return false; - // [].every() is true, so an empty plain object is a valid raw shape (matches v1). - return Object.values(obj).every(v => isZodV4Schema(v)); -} - -/** - * Accepts either a {@linkcode StandardSchemaWithJSON} or a raw Zod shape - * `{ field: z.string() }` and returns a {@linkcode StandardSchemaWithJSON}. - * Raw shapes are wrapped with `z.object()` so the rest of the pipeline sees a - * uniform schema type; already-wrapped schemas pass through unchanged. - * - * @internal - */ -export function normalizeRawShapeSchema( - schema: StandardSchemaWithJSON | Record | undefined -): StandardSchemaWithJSON | undefined { - if (schema === undefined) return undefined; - if (isZodRawShape(schema)) { - return z.object(schema) as StandardSchemaWithJSON; - } - if (typeof schema === 'object' && schema !== null && !isStandardSchema(schema) && Object.values(schema).some(v => looksLikeZodV3(v))) { - throw new TypeError( - 'Raw-shape inputSchema/outputSchema/argsSchema fields must be Zod v4 schemas. Got a Zod v3 field schema. Import from `zod/v4` (or upgrade your zod import), or wrap with `z.object({...})` yourself.' - ); - } - if (!isStandardSchema(schema)) { - throw new TypeError( - 'inputSchema/outputSchema/argsSchema must be a Standard Schema (e.g. z.object({...})) or a raw Zod shape ({ field: z.string() }).' - ); - } - // Any StandardSchema passes through; standardSchemaToJsonSchema owns the per-vendor - // handling for schemas without `~standard.jsonSchema` (zod 4.0-4.1 fallback, zod 3 - // and non-zod errors). Gating on `~standard.jsonSchema` here would unreachably - // front-run that fallback. - return schema; -} diff --git a/packages/core/src/validators/ajvProvider.examples.ts b/packages/core/src/validators/ajvProvider.examples.ts deleted file mode 100644 index 923d5a68ba..0000000000 --- a/packages/core/src/validators/ajvProvider.examples.ts +++ /dev/null @@ -1,46 +0,0 @@ -/** - * Type-checked examples for `ajvProvider.ts`. - * - * These examples are synced into JSDoc comments via the sync-snippets script. - * Each function's region markers define the code snippet that appears in the docs. - * - * @module - */ - -import { addFormats, Ajv, AjvJsonSchemaValidator } from './ajvProvider'; - -/** - * Example: Default AJV instance. - */ -function AjvJsonSchemaValidator_default() { - //#region AjvJsonSchemaValidator_default - const validator = new AjvJsonSchemaValidator(); - //#endregion AjvJsonSchemaValidator_default - return validator; -} - -/** - * Example: Custom AJV instance. - */ -function AjvJsonSchemaValidator_customInstance() { - //#region AjvJsonSchemaValidator_customInstance - const ajv = new Ajv({ strict: true, allErrors: true }); - const validator = new AjvJsonSchemaValidator(ajv); - //#endregion AjvJsonSchemaValidator_customInstance - return validator; -} - -/** - * Example: Custom AJV instance with formats registered. - * - * `Ajv` and `addFormats` are re-exported from this module so customising the validator - * requires no extra `package.json` dependencies — both come from the SDK's bundled copy. - */ -function AjvJsonSchemaValidator_withFormats() { - //#region AjvJsonSchemaValidator_withFormats - const ajv = new Ajv({ strict: true, allErrors: true }); - addFormats(ajv); - const validator = new AjvJsonSchemaValidator(ajv); - //#endregion AjvJsonSchemaValidator_withFormats - return validator; -} diff --git a/packages/core/src/validators/ajvProvider.ts b/packages/core/src/validators/ajvProvider.ts deleted file mode 100644 index 23d64dac7c..0000000000 --- a/packages/core/src/validators/ajvProvider.ts +++ /dev/null @@ -1,99 +0,0 @@ -/** - * AJV-based JSON Schema validator provider - */ - -import { Ajv } from 'ajv'; -import _addFormats from 'ajv-formats'; - -import type { JsonSchemaType, JsonSchemaValidator, jsonSchemaValidator, JsonSchemaValidatorResult } from './types'; - -/** Structural subset of the AJV interface used by {@link AjvJsonSchemaValidator}. */ -interface AjvLike { - compile: (schema: unknown) => AjvValidateFunction; - getSchema: (keyRef: string) => AjvValidateFunction | undefined; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - errorsText: (errors?: any) => string; -} - -interface AjvValidateFunction { - (input: unknown): boolean; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - errors?: any; -} - -function createDefaultAjvInstance(): Ajv { - const ajv = new Ajv({ - strict: false, - validateFormats: true, - validateSchema: false, - allErrors: true - }); - - const addFormats = _addFormats as unknown as typeof _addFormats.default; - addFormats(ajv); - - return ajv; -} - -/** - * AJV-backed JSON Schema validator. See `@modelcontextprotocol/{client,server}/validators/ajv` - * for the customisation entry point (re-exports `Ajv` and `addFormats` from the bundled copy). - * - * @example Use with default configuration - * ```ts source="./ajvProvider.examples.ts#AjvJsonSchemaValidator_default" - * const validator = new AjvJsonSchemaValidator(); - * ``` - * - * @example Use with a custom AJV instance - * ```ts source="./ajvProvider.examples.ts#AjvJsonSchemaValidator_customInstance" - * const ajv = new Ajv({ strict: true, allErrors: true }); - * const validator = new AjvJsonSchemaValidator(ajv); - * ``` - * - * @example Register ajv-formats - * ```ts source="./ajvProvider.examples.ts#AjvJsonSchemaValidator_withFormats" - * const ajv = new Ajv({ strict: true, allErrors: true }); - * addFormats(ajv); - * const validator = new AjvJsonSchemaValidator(ajv); - * ``` - */ -export class AjvJsonSchemaValidator implements jsonSchemaValidator { - private _ajv: AjvLike; - - /** - * @param ajv - Optional pre-configured AJV-compatible instance. If omitted, a default instance is - * created with `strict: false`, `validateFormats: true`, `validateSchema: false`, `allErrors: true`, - * and `ajv-formats` registered. The parameter is typed structurally so consumers who don't pass - * an instance need not have `ajv` installed. - */ - constructor(ajv?: AjvLike) { - this._ajv = ajv ?? createDefaultAjvInstance(); - } - - getValidator(schema: JsonSchemaType): JsonSchemaValidator { - const ajvValidator = - '$id' in schema && typeof schema.$id === 'string' - ? (this._ajv.getSchema(schema.$id) ?? this._ajv.compile(schema)) - : this._ajv.compile(schema); - - return (input: unknown): JsonSchemaValidatorResult => { - const valid = ajvValidator(input); - - return valid - ? { - valid: true, - data: input as T, - errorMessage: undefined - } - : { - valid: false, - data: undefined, - errorMessage: this._ajv.errorsText(ajvValidator.errors) - }; - }; - } -} - -export { Ajv } from 'ajv'; -/** `ajv-formats` default export, normalised through the CJS/ESM interop wrapper. */ -export const addFormats = _addFormats as unknown as typeof _addFormats.default; diff --git a/packages/core/src/validators/cfWorkerProvider.examples.ts b/packages/core/src/validators/cfWorkerProvider.examples.ts deleted file mode 100644 index facc971b0c..0000000000 --- a/packages/core/src/validators/cfWorkerProvider.examples.ts +++ /dev/null @@ -1,33 +0,0 @@ -/** - * Type-checked examples for `cfWorkerProvider.ts`. - * - * These examples are synced into JSDoc comments via the sync-snippets script. - * Each function's region markers define the code snippet that appears in the docs. - * - * @module - */ - -import { CfWorkerJsonSchemaValidator } from './cfWorkerProvider'; - -/** - * Example: Default configuration (draft 2020-12, shortcircuit on). - */ -function CfWorkerJsonSchemaValidator_default() { - //#region CfWorkerJsonSchemaValidator_default - const validator = new CfWorkerJsonSchemaValidator(); - //#endregion CfWorkerJsonSchemaValidator_default - return validator; -} - -/** - * Example: Custom configuration with all errors reported. - */ -function CfWorkerJsonSchemaValidator_customConfig() { - //#region CfWorkerJsonSchemaValidator_customConfig - const validator = new CfWorkerJsonSchemaValidator({ - draft: '2020-12', - shortcircuit: false // Report all errors - }); - //#endregion CfWorkerJsonSchemaValidator_customConfig - return validator; -} diff --git a/packages/core/src/validators/cfWorkerProvider.ts b/packages/core/src/validators/cfWorkerProvider.ts deleted file mode 100644 index c3cfb34481..0000000000 --- a/packages/core/src/validators/cfWorkerProvider.ts +++ /dev/null @@ -1,81 +0,0 @@ -/** - * Cloudflare Worker-compatible JSON Schema validator provider - * - * This provider uses @cfworker/json-schema for validation without code generation, - * making it compatible with edge runtimes like Cloudflare Workers that restrict - * eval and new Function. - * - * @see {@linkcode AjvJsonSchemaValidator} for the Node.js alternative - */ - -import { Validator } from '@cfworker/json-schema'; - -import type { JsonSchemaType, JsonSchemaValidator, jsonSchemaValidator, JsonSchemaValidatorResult } from './types'; - -/** - * JSON Schema draft version supported by `@cfworker/json-schema`. - */ -export type CfWorkerSchemaDraft = '4' | '7' | '2019-09' | '2020-12'; - -/** - * `@cfworker/json-schema`-backed JSON Schema validator. See - * `@modelcontextprotocol/{client,server}/validators/cf-worker` for the customisation entry point. - * - * @example Use with default configuration (draft 2020-12, shortcircuit on) - * ```ts source="./cfWorkerProvider.examples.ts#CfWorkerJsonSchemaValidator_default" - * const validator = new CfWorkerJsonSchemaValidator(); - * ``` - * - * @example Use with custom configuration - * ```ts source="./cfWorkerProvider.examples.ts#CfWorkerJsonSchemaValidator_customConfig" - * const validator = new CfWorkerJsonSchemaValidator({ - * draft: '2020-12', - * shortcircuit: false // Report all errors - * }); - * ``` - */ -export class CfWorkerJsonSchemaValidator implements jsonSchemaValidator { - private shortcircuit: boolean; - private draft: CfWorkerSchemaDraft; - - /** - * Create a validator - * - * @param options - Configuration options - * @param options.shortcircuit - If `true`, stop validation after first error (default: `true`) - * @param options.draft - JSON Schema draft version to use (default: `'2020-12'`) - */ - constructor(options?: { shortcircuit?: boolean; draft?: CfWorkerSchemaDraft }) { - this.shortcircuit = options?.shortcircuit ?? true; - this.draft = options?.draft ?? '2020-12'; - } - - /** - * Create a validator for the given JSON Schema - * - * Unlike AJV, this validator is not cached internally - * - * @param schema - Standard JSON Schema object - * @returns A validator function that validates input data - */ - getValidator(schema: JsonSchemaType): JsonSchemaValidator { - // Cast to the cfworker Schema type - our JsonSchemaType is structurally compatible - const validator = new Validator(schema as ConstructorParameters[0], this.draft, this.shortcircuit); - - return (input: unknown): JsonSchemaValidatorResult => { - const result = validator.validate(input); - - return result.valid - ? { - valid: true, - data: input as T, - errorMessage: undefined - } - : { - valid: false, - data: undefined, - errorMessage: result.errors.map(err => `${err.instanceLocation}: ${err.error}`).join('; ') - }; - }; - } -} diff --git a/packages/core/src/validators/fromJsonSchema.examples.ts b/packages/core/src/validators/fromJsonSchema.examples.ts deleted file mode 100644 index 7df661c545..0000000000 --- a/packages/core/src/validators/fromJsonSchema.examples.ts +++ /dev/null @@ -1,30 +0,0 @@ -/** - * Type-checked examples for `fromJsonSchema.ts`. - * - * These examples are synced into JSDoc comments via the sync-snippets script. - * - * @module - */ - -import { fromJsonSchema } from './fromJsonSchema'; -import type { jsonSchemaValidator } from './types'; - -declare const validator: jsonSchemaValidator; - -/** - * Example: wrap a raw JSON Schema object for use with registerTool. - * - * Consumers importing `fromJsonSchema` from `@modelcontextprotocol/server` or - * `@modelcontextprotocol/client` omit the second argument — the runtime shim - * supplies the appropriate default validator. - */ -function fromJsonSchema_basicUsage() { - //#region fromJsonSchema_basicUsage - const inputSchema = fromJsonSchema<{ name: string }>( - { type: 'object', properties: { name: { type: 'string' } }, required: ['name'] }, - validator - ); - // Use with server.registerTool('greet', { inputSchema }, handler) - //#endregion fromJsonSchema_basicUsage - return inputSchema; -} diff --git a/packages/core/src/validators/fromJsonSchema.ts b/packages/core/src/validators/fromJsonSchema.ts deleted file mode 100644 index 696d1f29e9..0000000000 --- a/packages/core/src/validators/fromJsonSchema.ts +++ /dev/null @@ -1,43 +0,0 @@ -import type { StandardSchemaV1, StandardSchemaWithJSON } from '../util/standardSchema'; -import type { JsonSchemaType, jsonSchemaValidator } from './types'; - -/** - * Wrap a raw JSON Schema object as a {@linkcode StandardSchemaWithJSON} so it can be - * passed to `registerTool` / `registerPrompt`. Use this when you already have JSON - * Schema (e.g. from TypeBox, or hand-written) and want to register it without going - * through a Standard Schema library. - * - * The callback arguments will be typed `unknown` (raw JSON Schema has no TypeScript - * types attached). Cast at the call site, or use the generic `fromJsonSchema(...)`. - * - * @param schema - A JSON Schema object describing the expected shape - * @param validator - A validator provider. When importing `fromJsonSchema` from - * `@modelcontextprotocol/server` or `@modelcontextprotocol/client`, a runtime-appropriate - * default is provided automatically (AJV on Node.js, CfWorker on edge runtimes). - * - * @example - * ```ts source="./fromJsonSchema.examples.ts#fromJsonSchema_basicUsage" - * const inputSchema = fromJsonSchema<{ name: string }>( - * { type: 'object', properties: { name: { type: 'string' } }, required: ['name'] }, - * validator - * ); - * // Use with server.registerTool('greet', { inputSchema }, handler) - * ``` - */ -export function fromJsonSchema(schema: JsonSchemaType, validator: jsonSchemaValidator): StandardSchemaWithJSON { - const check = validator.getValidator(schema); - return { - '~standard': { - version: 1, - vendor: 'mcp', - jsonSchema: { - input: () => schema as Record, - output: () => schema as Record - }, - validate: (data: unknown): StandardSchemaV1.Result => { - const result = check(data); - return result.valid ? { value: result.data } : { issues: [{ message: result.errorMessage }] }; - } - } - }; -} diff --git a/packages/core/src/validators/types.examples.ts b/packages/core/src/validators/types.examples.ts deleted file mode 100644 index 2066a8aff3..0000000000 --- a/packages/core/src/validators/types.examples.ts +++ /dev/null @@ -1,31 +0,0 @@ -/** - * Type-checked examples for `types.ts`. - * - * These examples are synced into JSDoc comments via the sync-snippets script. - * Each function's region markers define the code snippet that appears in the docs. - * - * @module - */ - -import type { JsonSchemaType, JsonSchemaValidator, jsonSchemaValidator } from './types'; - -// Stub for hypothetical schema validation function -declare function isValid(schema: JsonSchemaType, input: unknown): boolean; - -/** - * Example: Implementing the jsonSchemaValidator interface. - */ -function jsonSchemaValidator_implementation() { - //#region jsonSchemaValidator_implementation - class MyValidatorProvider implements jsonSchemaValidator { - getValidator(schema: JsonSchemaType): JsonSchemaValidator { - // Compile/cache validator from schema - return (input: unknown) => - isValid(schema, input) - ? { valid: true, data: input as T, errorMessage: undefined } - : { valid: false, data: undefined, errorMessage: 'Error details' }; - } - } - //#endregion jsonSchemaValidator_implementation - return MyValidatorProvider; -} diff --git a/packages/core/src/validators/types.ts b/packages/core/src/validators/types.ts deleted file mode 100644 index e2202b4a69..0000000000 --- a/packages/core/src/validators/types.ts +++ /dev/null @@ -1,59 +0,0 @@ -// Using the main export which points to draft-2020-12 by default -import type { JSONSchema } from 'json-schema-typed'; - -/** - * JSON Schema type definition (JSON Schema Draft 2020-12) - * - * This uses the object form of JSON Schema (excluding boolean schemas). - * While `true` and `false` are valid JSON Schemas, this SDK uses the - * object form for practical type safety. - * - * Re-exported from json-schema-typed for convenience. - * @see https://json-schema.org/draft/2020-12/json-schema-core.html - */ -export type JsonSchemaType = JSONSchema.Interface; - -/** - * Result of a JSON Schema validation operation - */ -export type JsonSchemaValidatorResult = - | { valid: true; data: T; errorMessage: undefined } - | { valid: false; data: undefined; errorMessage: string }; - -/** - * A validator function that validates data against a JSON Schema - */ -export type JsonSchemaValidator = (input: unknown) => JsonSchemaValidatorResult; - -/** - * Provider interface for creating validators from JSON Schemas - * - * This is the main extension point for custom validator implementations. - * Implementations should: - * - Support JSON Schema Draft 2020-12 (or be compatible with it) - * - Return validator functions that can be called multiple times - * - Handle schema compilation/caching internally - * - Provide clear error messages on validation failure - * - * @example - * ```ts source="./types.examples.ts#jsonSchemaValidator_implementation" - * class MyValidatorProvider implements jsonSchemaValidator { - * getValidator(schema: JsonSchemaType): JsonSchemaValidator { - * // Compile/cache validator from schema - * return (input: unknown) => - * isValid(schema, input) - * ? { valid: true, data: input as T, errorMessage: undefined } - * : { valid: false, data: undefined, errorMessage: 'Error details' }; - * } - * } - * ``` - */ -export interface jsonSchemaValidator { - /** - * Create a validator for the given JSON Schema - * - * @param schema - Standard JSON Schema object - * @returns A validator function that can be called multiple times - */ - getValidator(schema: JsonSchemaType): JsonSchemaValidator; -} diff --git a/packages/sdk-shared/test/sdkSharedSchemas.test.ts b/packages/core/test/coreSchemas.test.ts similarity index 87% rename from packages/sdk-shared/test/sdkSharedSchemas.test.ts rename to packages/core/test/coreSchemas.test.ts index cc7d3fa0be..3f57ae6e2b 100644 --- a/packages/sdk-shared/test/sdkSharedSchemas.test.ts +++ b/packages/core/test/coreSchemas.test.ts @@ -14,7 +14,7 @@ function exportedSchemaConsts(src: string, re: RegExp): string[] { return [...src.matchAll(re)].map(m => m[1]).filter((name): name is string => name !== undefined && /^[A-Z]/.test(name)); } -describe('@modelcontextprotocol/sdk-shared', () => { +describe('@modelcontextprotocol/core', () => { it('re-exports spec + OAuth schemas as working Zod objects', () => { // Round-trips valid/invalid values — proves the re-exports are real Zod schemas (not type-only // aliases) and that `.parse`/`.safeParse` work, for both the spec and the OAuth group. @@ -25,7 +25,7 @@ describe('@modelcontextprotocol/sdk-shared', () => { }); it('re-exports exactly core’s spec + OAuth schemas — no internal helpers (drift guard)', () => { - // sdk-shared's public surface is two SEPARATE groups, mirroring core's own spec-vs-auth split: + // core's public surface is two SEPARATE groups, mirroring core's own spec-vs-auth split: // 1. spec `*Schema` constants from core/src/types/schemas.ts (minus internal helpers with no // public spec type — they must NOT leak), mirroring core's SPEC_SCHEMA_KEYS allowlist; and // 2. the auth `*Schema` constants registered in core's `authSchemas` object (specTypeSchema.ts) @@ -41,10 +41,11 @@ describe('@modelcontextprotocol/sdk-shared', () => { 'NotificationsParamsSchema', 'ServerTasksCapabilitySchema' ]; - const specSchemas = exportedSchemaConsts(readCore('../../core/src/types/schemas.ts'), /^export const (\w+Schema)\b/gm).filter( - name => !SPEC_INTERNAL_HELPERS.includes(name) - ); - const specTypeSrc = readCore('../../core/src/types/specTypeSchema.ts'); + const specSchemas = exportedSchemaConsts( + readCore('../../core-internal/src/types/schemas.ts'), + /^export const (\w+Schema)\b/gm + ).filter(name => !SPEC_INTERNAL_HELPERS.includes(name)); + const specTypeSrc = readCore('../../core-internal/src/types/specTypeSchema.ts'); const authStart = specTypeSrc.indexOf('const authSchemas = {'); const authObj = specTypeSrc.slice(authStart, specTypeSrc.indexOf('} as const', authStart)); const authSchemas = exportedSchemaConsts(authObj, /\b(\w+Schema)\b/g); diff --git a/packages/core/test/errors/sdkHttpError.test.ts b/packages/core/test/errors/sdkHttpError.test.ts deleted file mode 100644 index 421a51d6c6..0000000000 --- a/packages/core/test/errors/sdkHttpError.test.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { describe, it, expect } from 'vitest'; -import { SdkError, SdkErrorCode, SdkHttpError } from '../../src/index'; - -describe('SdkHttpError', () => { - it('exposes status and statusText via getters', () => { - const error = new SdkHttpError(SdkErrorCode.ClientHttpAuthentication, 'Unauthorized', { - status: 401, - statusText: 'Unauthorized' - }); - - expect(error.status).toBe(401); - expect(error.statusText).toBe('Unauthorized'); - }); - - it('returns undefined for statusText when omitted', () => { - const error = new SdkHttpError(SdkErrorCode.ClientHttpAuthentication, 'auth failed', { - status: 401 - }); - - expect(error.status).toBe(401); - expect(error.statusText).toBeUndefined(); - }); - - it('is an instance of SdkError', () => { - const error = new SdkHttpError(SdkErrorCode.ClientHttpForbidden, 'Forbidden', { - status: 403, - statusText: 'Forbidden' - }); - - expect(error).toBeInstanceOf(SdkError); - expect(error).toBeInstanceOf(SdkHttpError); - }); - - it('preserves code and message from SdkError', () => { - const error = new SdkHttpError(SdkErrorCode.ClientHttpNotImplemented, 'Not Implemented', { - status: 501, - statusText: 'Not Implemented' - }); - - expect(error.code).toBe(SdkErrorCode.ClientHttpNotImplemented); - expect(error.message).toBe('Not Implemented'); - expect(error.name).toBe('SdkHttpError'); - }); - - it('exposes extra data fields alongside status', () => { - const error = new SdkHttpError(SdkErrorCode.ClientHttpAuthentication, 'auth failed', { - status: 401, - statusText: 'Unauthorized', - retryAfter: 30 - }); - - expect(error.data.retryAfter).toBe(30); - expect(error.status).toBe(401); - }); -}); diff --git a/packages/core/test/inMemory.test.ts b/packages/core/test/inMemory.test.ts deleted file mode 100644 index 0dc72fd32f..0000000000 --- a/packages/core/test/inMemory.test.ts +++ /dev/null @@ -1,165 +0,0 @@ -import type { AuthInfo, JSONRPCMessage } from '../src/types/index'; -import { InMemoryTransport } from '../src/util/inMemory'; - -describe('InMemoryTransport', () => { - let clientTransport: InMemoryTransport; - let serverTransport: InMemoryTransport; - - beforeEach(() => { - [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); - }); - - test('should create linked pair', () => { - expect(clientTransport).toBeDefined(); - expect(serverTransport).toBeDefined(); - }); - - test('should start without error', async () => { - await expect(clientTransport.start()).resolves.not.toThrow(); - await expect(serverTransport.start()).resolves.not.toThrow(); - }); - - test('should send message from client to server', async () => { - const message: JSONRPCMessage = { - jsonrpc: '2.0', - method: 'test', - id: 1 - }; - - let receivedMessage: JSONRPCMessage | undefined; - serverTransport.onmessage = msg => { - receivedMessage = msg; - }; - - await clientTransport.send(message); - expect(receivedMessage).toEqual(message); - }); - - test('should send message with auth info from client to server', async () => { - const message: JSONRPCMessage = { - jsonrpc: '2.0', - method: 'test', - id: 1 - }; - - const authInfo: AuthInfo = { - token: 'test-token', - clientId: 'test-client', - scopes: ['read', 'write'], - expiresAt: Date.now() / 1000 + 3600 - }; - - let receivedMessage: JSONRPCMessage | undefined; - let receivedAuthInfo: AuthInfo | undefined; - serverTransport.onmessage = (msg, extra) => { - receivedMessage = msg; - receivedAuthInfo = extra?.authInfo; - }; - - await clientTransport.send(message, { authInfo }); - expect(receivedMessage).toEqual(message); - expect(receivedAuthInfo).toEqual(authInfo); - }); - - test('should send message from server to client', async () => { - const message: JSONRPCMessage = { - jsonrpc: '2.0', - method: 'test', - id: 1 - }; - - let receivedMessage: JSONRPCMessage | undefined; - clientTransport.onmessage = msg => { - receivedMessage = msg; - }; - - await serverTransport.send(message); - expect(receivedMessage).toEqual(message); - }); - - test('should handle close', async () => { - let clientClosed = false; - let serverClosed = false; - - clientTransport.onclose = () => { - clientClosed = true; - }; - - serverTransport.onclose = () => { - serverClosed = true; - }; - - await clientTransport.close(); - expect(clientClosed).toBe(true); - expect(serverClosed).toBe(true); - }); - - test('should throw error when sending after close', async () => { - await clientTransport.close(); - await expect(clientTransport.send({ jsonrpc: '2.0', method: 'test', id: 1 })).rejects.toThrow('Not connected'); - }); - - test('should fire onclose exactly once per transport', async () => { - let clientCloseCount = 0; - let serverCloseCount = 0; - - clientTransport.onclose = () => clientCloseCount++; - serverTransport.onclose = () => serverCloseCount++; - - await clientTransport.close(); - - expect(clientCloseCount).toBe(1); - expect(serverCloseCount).toBe(1); - }); - - test('should handle double close idempotently', async () => { - let clientCloseCount = 0; - clientTransport.onclose = () => clientCloseCount++; - - await clientTransport.close(); - await clientTransport.close(); - - expect(clientCloseCount).toBe(1); - }); - - test('should handle concurrent close from both sides', async () => { - let clientCloseCount = 0; - let serverCloseCount = 0; - - clientTransport.onclose = () => clientCloseCount++; - serverTransport.onclose = () => serverCloseCount++; - - await Promise.all([clientTransport.close(), serverTransport.close()]); - - expect(clientCloseCount).toBe(1); - expect(serverCloseCount).toBe(1); - }); - - test('should fire onclose even if peer onclose throws', async () => { - let clientCloseCount = 0; - clientTransport.onclose = () => clientCloseCount++; - serverTransport.onclose = () => { - throw new Error('boom'); - }; - - await expect(clientTransport.close()).rejects.toThrow('boom'); - expect(clientCloseCount).toBe(1); - }); - - test('should queue messages sent before start', async () => { - const message: JSONRPCMessage = { - jsonrpc: '2.0', - method: 'test', - id: 1 - }; - - let receivedMessage: JSONRPCMessage | undefined; - serverTransport.onmessage = msg => { - receivedMessage = msg; - }; - - await clientTransport.send(message); - await serverTransport.start(); - expect(receivedMessage).toEqual(message); - }); -}); diff --git a/packages/core/test/shared/auth.test.ts b/packages/core/test/shared/auth.test.ts deleted file mode 100644 index 10c9462c9a..0000000000 --- a/packages/core/test/shared/auth.test.ts +++ /dev/null @@ -1,122 +0,0 @@ -import { - OAuthClientMetadataSchema, - OAuthMetadataSchema, - OpenIdProviderMetadataSchema, - OptionalSafeUrlSchema, - SafeUrlSchema -} from '../../src/shared/auth'; - -describe('SafeUrlSchema', () => { - it('accepts valid HTTPS URLs', () => { - expect(SafeUrlSchema.parse('https://example.com')).toBe('https://example.com'); - expect(SafeUrlSchema.parse('https://auth.example.com/oauth/authorize')).toBe('https://auth.example.com/oauth/authorize'); - }); - - it('accepts valid HTTP URLs', () => { - expect(SafeUrlSchema.parse('http://localhost:3000')).toBe('http://localhost:3000'); - }); - - it('rejects javascript: scheme URLs', () => { - expect(() => SafeUrlSchema.parse('javascript:alert(1)')).toThrow('URL cannot use javascript:, data:, or vbscript: scheme'); - expect(() => SafeUrlSchema.parse('JAVASCRIPT:alert(1)')).toThrow('URL cannot use javascript:, data:, or vbscript: scheme'); - }); - - it('rejects invalid URLs', () => { - expect(() => SafeUrlSchema.parse('not-a-url')).toThrow(); - expect(() => SafeUrlSchema.parse('')).toThrow(); - }); - - it('works with safeParse', () => { - expect(() => SafeUrlSchema.safeParse('not-a-url')).not.toThrow(); - }); -}); - -describe('OptionalSafeUrlSchema', () => { - it('accepts empty string and transforms it to undefined', () => { - expect(OptionalSafeUrlSchema.parse('')).toBe(undefined); - }); -}); - -describe('OAuthMetadataSchema', () => { - it('validates complete OAuth metadata', () => { - const metadata = { - issuer: 'https://auth.example.com', - authorization_endpoint: 'https://auth.example.com/oauth/authorize', - token_endpoint: 'https://auth.example.com/oauth/token', - response_types_supported: ['code'], - scopes_supported: ['read', 'write'] - }; - - expect(() => OAuthMetadataSchema.parse(metadata)).not.toThrow(); - }); - - it('rejects metadata with javascript: URLs', () => { - const metadata = { - issuer: 'https://auth.example.com', - authorization_endpoint: 'javascript:alert(1)', - token_endpoint: 'https://auth.example.com/oauth/token', - response_types_supported: ['code'] - }; - - expect(() => OAuthMetadataSchema.parse(metadata)).toThrow('URL cannot use javascript:, data:, or vbscript: scheme'); - }); - - it('requires mandatory fields', () => { - const incompleteMetadata = { - issuer: 'https://auth.example.com' - }; - - expect(() => OAuthMetadataSchema.parse(incompleteMetadata)).toThrow(); - }); -}); - -describe('OpenIdProviderMetadataSchema', () => { - it('validates complete OpenID Provider metadata', () => { - const metadata = { - issuer: 'https://auth.example.com', - authorization_endpoint: 'https://auth.example.com/oauth/authorize', - token_endpoint: 'https://auth.example.com/oauth/token', - jwks_uri: 'https://auth.example.com/.well-known/jwks.json', - response_types_supported: ['code'], - subject_types_supported: ['public'], - id_token_signing_alg_values_supported: ['RS256'] - }; - - expect(() => OpenIdProviderMetadataSchema.parse(metadata)).not.toThrow(); - }); - - it('rejects metadata with javascript: in jwks_uri', () => { - const metadata = { - issuer: 'https://auth.example.com', - authorization_endpoint: 'https://auth.example.com/oauth/authorize', - token_endpoint: 'https://auth.example.com/oauth/token', - jwks_uri: 'javascript:alert(1)', - response_types_supported: ['code'], - subject_types_supported: ['public'], - id_token_signing_alg_values_supported: ['RS256'] - }; - - expect(() => OpenIdProviderMetadataSchema.parse(metadata)).toThrow('URL cannot use javascript:, data:, or vbscript: scheme'); - }); -}); - -describe('OAuthClientMetadataSchema', () => { - it('validates client metadata with safe URLs', () => { - const metadata = { - redirect_uris: ['https://app.example.com/callback'], - client_name: 'Test App', - client_uri: 'https://app.example.com' - }; - - expect(() => OAuthClientMetadataSchema.parse(metadata)).not.toThrow(); - }); - - it('rejects client metadata with javascript: redirect URIs', () => { - const metadata = { - redirect_uris: ['javascript:alert(1)'], - client_name: 'Test App' - }; - - expect(() => OAuthClientMetadataSchema.parse(metadata)).toThrow('URL cannot use javascript:, data:, or vbscript: scheme'); - }); -}); diff --git a/packages/core/test/shared/authUtils.test.ts b/packages/core/test/shared/authUtils.test.ts deleted file mode 100644 index 312d1809ef..0000000000 --- a/packages/core/test/shared/authUtils.test.ts +++ /dev/null @@ -1,90 +0,0 @@ -import { checkResourceAllowed, resourceUrlFromServerUrl } from '../../src/shared/authUtils'; - -describe('auth-utils', () => { - describe('resourceUrlFromServerUrl', () => { - it('should remove fragments', () => { - expect(resourceUrlFromServerUrl(new URL('https://example.com/path#fragment')).href).toBe('https://example.com/path'); - expect(resourceUrlFromServerUrl(new URL('https://example.com#fragment')).href).toBe('https://example.com/'); - expect(resourceUrlFromServerUrl(new URL('https://example.com/path?query=1#fragment')).href).toBe( - 'https://example.com/path?query=1' - ); - }); - - it('should return URL unchanged if no fragment', () => { - expect(resourceUrlFromServerUrl(new URL('https://example.com')).href).toBe('https://example.com/'); - expect(resourceUrlFromServerUrl(new URL('https://example.com/path')).href).toBe('https://example.com/path'); - expect(resourceUrlFromServerUrl(new URL('https://example.com/path?query=1')).href).toBe('https://example.com/path?query=1'); - }); - - it('should keep everything else unchanged', () => { - // Case sensitivity preserved - expect(resourceUrlFromServerUrl(new URL('https://EXAMPLE.COM/PATH')).href).toBe('https://example.com/PATH'); - // Ports preserved - expect(resourceUrlFromServerUrl(new URL('https://example.com:443/path')).href).toBe('https://example.com/path'); - expect(resourceUrlFromServerUrl(new URL('https://example.com:8080/path')).href).toBe('https://example.com:8080/path'); - // Query parameters preserved - expect(resourceUrlFromServerUrl(new URL('https://example.com?foo=bar&baz=qux')).href).toBe( - 'https://example.com/?foo=bar&baz=qux' - ); - // Trailing slashes preserved - expect(resourceUrlFromServerUrl(new URL('https://example.com/')).href).toBe('https://example.com/'); - expect(resourceUrlFromServerUrl(new URL('https://example.com/path/')).href).toBe('https://example.com/path/'); - }); - }); - - describe('resourceMatches', () => { - it('should match identical URLs', () => { - expect( - checkResourceAllowed({ requestedResource: 'https://example.com/path', configuredResource: 'https://example.com/path' }) - ).toBe(true); - expect(checkResourceAllowed({ requestedResource: 'https://example.com/', configuredResource: 'https://example.com/' })).toBe( - true - ); - }); - - it('should not match URLs with different paths', () => { - expect( - checkResourceAllowed({ requestedResource: 'https://example.com/path1', configuredResource: 'https://example.com/path2' }) - ).toBe(false); - expect( - checkResourceAllowed({ requestedResource: 'https://example.com/', configuredResource: 'https://example.com/path' }) - ).toBe(false); - }); - - it('should not match URLs with different domains', () => { - expect( - checkResourceAllowed({ requestedResource: 'https://example.com/path', configuredResource: 'https://example.org/path' }) - ).toBe(false); - }); - - it('should not match URLs with different ports', () => { - expect( - checkResourceAllowed({ requestedResource: 'https://example.com:8080/path', configuredResource: 'https://example.com/path' }) - ).toBe(false); - }); - - it('should not match URLs where one path is a sub-path of another', () => { - expect( - checkResourceAllowed({ requestedResource: 'https://example.com/mcpxxxx', configuredResource: 'https://example.com/mcp' }) - ).toBe(false); - expect( - checkResourceAllowed({ - requestedResource: 'https://example.com/folder', - configuredResource: 'https://example.com/folder/subfolder' - }) - ).toBe(false); - expect( - checkResourceAllowed({ requestedResource: 'https://example.com/api/v1', configuredResource: 'https://example.com/api' }) - ).toBe(true); - }); - - it('should handle trailing slashes vs no trailing slashes', () => { - expect( - checkResourceAllowed({ requestedResource: 'https://example.com/mcp/', configuredResource: 'https://example.com/mcp' }) - ).toBe(true); - expect( - checkResourceAllowed({ requestedResource: 'https://example.com/folder', configuredResource: 'https://example.com/folder/' }) - ).toBe(false); - }); - }); -}); diff --git a/packages/core/test/shared/customMethods.test.ts b/packages/core/test/shared/customMethods.test.ts deleted file mode 100644 index 624d6bfdd1..0000000000 --- a/packages/core/test/shared/customMethods.test.ts +++ /dev/null @@ -1,198 +0,0 @@ -import { describe, expect, it } from 'vitest'; -import { z } from 'zod/v4'; - -import { Protocol } from '../../src/shared/protocol'; -import type { BaseContext, JSONRPCRequest, Result, StandardSchemaV1 } from '../../src/exports/public/index'; -import { ProtocolError } from '../../src/types/index'; -import { SdkErrorCode } from '../../src/errors/sdkErrors'; -import { InMemoryTransport } from '../../src/util/inMemory'; - -class TestProtocol extends Protocol { - protected buildContext(ctx: BaseContext): BaseContext { - return ctx; - } - protected assertCapabilityForMethod(): void {} - protected assertNotificationCapability(): void {} - protected assertRequestHandlerCapability(): void {} -} - -async function pair(): Promise<[TestProtocol, TestProtocol]> { - const [t1, t2] = InMemoryTransport.createLinkedPair(); - const a = new TestProtocol(); - const b = new TestProtocol(); - await a.connect(t1); - await b.connect(t2); - return [a, b]; -} - -describe('Protocol custom-method support', () => { - describe('setRequestHandler 3-arg form', () => { - const SearchParams = z.object({ query: z.string(), limit: z.number().int() }); - const SearchResult = z.object({ items: z.array(z.string()) }); - - it('registers, validates params, and handler receives parsed params', async () => { - const [a, b] = await pair(); - b.setRequestHandler('acme/search', { params: SearchParams, result: SearchResult }, async (params, _ctx) => { - expect(params.query).toBe('hello'); - expect(params.limit).toBe(5); - return { items: [`result for ${params.query}`] }; - }); - - const result = await a.request({ method: 'acme/search', params: { query: 'hello', limit: 5 } }, SearchResult); - expect(result.items).toEqual(['result for hello']); - }); - - it('strips _meta from params before validation', async () => { - const [a, b] = await pair(); - const Strict = z.strictObject({ x: z.number() }); - b.setRequestHandler('acme/strict', { params: Strict }, async params => { - expect(params).toEqual({ x: 1 }); - return {}; - }); - - const result = await a.request({ method: 'acme/strict', params: { x: 1, _meta: { progressToken: 't' } } }, z.object({})); - expect(result).toEqual({}); - }); - - it('rejects invalid params with ProtocolError(InvalidParams)', async () => { - const [a, b] = await pair(); - b.setRequestHandler('acme/search', { params: SearchParams }, async () => ({})); - - await expect(a.request({ method: 'acme/search', params: { query: 'q', limit: 'oops' } }, z.object({}))).rejects.toThrow( - ProtocolError - ); - }); - - it('types handler return from schemas.result', () => { - const p = new TestProtocol(); - p.setRequestHandler('acme/typed', { params: z.object({}), result: SearchResult }, async () => { - return { items: [] }; - }); - // @ts-expect-error wrong return shape when result schema supplied - p.setRequestHandler('acme/typed', { params: z.object({}), result: SearchResult }, async () => ({})); - // No result schema → handler may return any Result - p.setRequestHandler('acme/loose', { params: z.object({}) }, async () => ({}) as Result); - }); - - it('throws TypeError when 2-arg form is used with a non-spec method', () => { - const p = new TestProtocol(); - expect(() => p.setRequestHandler('acme/unknown' as never, () => ({}) as never)).toThrow(TypeError); - }); - - it('routes both 2-arg and 3-arg registration through _wrapHandler', () => { - const seen: string[] = []; - class SpyProtocol extends TestProtocol { - protected override _wrapHandler( - method: string, - handler: (request: JSONRPCRequest, ctx: BaseContext) => Promise - ): (request: JSONRPCRequest, ctx: BaseContext) => Promise { - seen.push(method); - return handler; - } - } - const p = new SpyProtocol(); - p.setRequestHandler('tools/list', () => ({ tools: [] })); - p.setRequestHandler('acme/custom', { params: z.object({}) }, () => ({})); - expect(seen).toContain('tools/list'); - expect(seen).toContain('acme/custom'); - }); - }); - - describe('setNotificationHandler 3-arg form', () => { - it('registers, validates params, handler receives parsed params', async () => { - const [a, b] = await pair(); - const Progress = z.object({ stage: z.string(), pct: z.number() }); - const seen: Array> = []; - b.setNotificationHandler('acme/searchProgress', { params: Progress }, params => { - seen.push(params); - }); - - await a.notification({ method: 'acme/searchProgress', params: { stage: 'fetch', pct: 0.5 } }); - await new Promise(r => setTimeout(r, 0)); - expect(seen).toEqual([{ stage: 'fetch', pct: 0.5 }]); - }); - - it('passes the raw notification (with _meta) as the second handler argument', async () => { - const [a, b] = await pair(); - const Strict = z.strictObject({ stage: z.string() }); - let seenMeta: unknown; - b.setNotificationHandler('acme/searchProgress', { params: Strict }, (params, notification) => { - expect(params).toEqual({ stage: 'fetch' }); - seenMeta = notification.params?._meta; - }); - - await a.notification({ method: 'acme/searchProgress', params: { stage: 'fetch', _meta: { traceId: 't1' } } }); - await new Promise(r => setTimeout(r, 0)); - expect(seenMeta).toEqual({ traceId: 't1' }); - }); - }); - - describe('request() schema overload', () => { - it('validates result against provided schema and types the return', async () => { - const [a, b] = await pair(); - b.setRequestHandler('acme/echo', { params: z.object({ v: z.string() }) }, async params => ({ echoed: params.v })); - - const result = await a.request({ method: 'acme/echo', params: { v: 'x' } }, z.object({ echoed: z.string() })); - expect(result.echoed).toBe('x'); - }); - - it('throws TypeError when 1-arg form is used with a non-spec method', async () => { - const [a] = await pair(); - expect(() => a.request({ method: 'acme/unknown' } as never)).toThrow(TypeError); - }); - - it('rejects with SdkError(InvalidResult) when the response fails the result schema', async () => { - const [a, b] = await pair(); - b.setRequestHandler('acme/bad', { params: z.object({}) }, async () => ({ wrong: 123 })); - - await expect(a.request({ method: 'acme/bad', params: {} }, z.object({ echoed: z.string() }))).rejects.toMatchObject({ - code: SdkErrorCode.InvalidResult - }); - }); - - it('returns the result (and sends no cancellation) if the signal aborts during async result-schema validation', async () => { - const [a, b] = await pair(); - b.setRequestHandler('acme/echo', { params: z.object({}) }, async () => ({ echoed: 'ok' })); - - const cancelled: unknown[] = []; - b.setNotificationHandler('notifications/cancelled', n => { - cancelled.push(n); - }); - - const ac = new AbortController(); - const AsyncEcho: StandardSchemaV1 = { - '~standard': { - version: 1, - vendor: 'test', - validate: value => - new Promise(r => { - ac.abort(); - setTimeout(() => r({ value: value as { echoed: string } }), 0); - }) - } - }; - - const result = await a.request({ method: 'acme/echo', params: {} }, AsyncEcho, { signal: ac.signal }); - expect(result).toEqual({ echoed: 'ok' }); - await new Promise(r => setTimeout(r, 0)); - expect(cancelled).toHaveLength(0); - }); - }); - - describe('ctx.mcpReq.send schema overload', () => { - it('sends a related custom-method request from within a handler', async () => { - const [a, b] = await pair(); - const Pong = z.object({ pong: z.literal(true) }); - - a.setRequestHandler('acme/pong', { params: z.object({}) }, async () => ({ pong: true as const })); - b.setRequestHandler('acme/ping', { params: z.object({}) }, async (_params, ctx) => { - const r = await ctx.mcpReq.send({ method: 'acme/pong', params: {} }, Pong); - expect(r.pong).toBe(true); - return { ok: true }; - }); - - const result = await a.request({ method: 'acme/ping', params: {} }, z.object({ ok: z.boolean() })); - expect(result.ok).toBe(true); - }); - }); -}); diff --git a/packages/core/test/shared/protocol.test.ts b/packages/core/test/shared/protocol.test.ts deleted file mode 100644 index 6f0eda2912..0000000000 --- a/packages/core/test/shared/protocol.test.ts +++ /dev/null @@ -1,912 +0,0 @@ -import type { MockInstance } from 'vitest'; -import { vi } from 'vitest'; -import * as z from 'zod/v4'; -import type { ZodType } from 'zod/v4'; - -import type { BaseContext } from '../../src/shared/protocol'; -import { mergeCapabilities, Protocol } from '../../src/shared/protocol'; -import type { Transport, TransportSendOptions } from '../../src/shared/transport'; -import type { - ClientCapabilities, - JSONRPCErrorResponse, - JSONRPCMessage, - JSONRPCNotification, - JSONRPCRequest, - JSONRPCResponse, - JSONRPCResultResponse, - Notification, - Request, - RequestId, - Result, - ServerCapabilities -} from '../../src/types/index'; -import { ProtocolError, ProtocolErrorCode } from '../../src/types/index'; -import { SdkError, SdkErrorCode } from '../../src/errors/sdkErrors'; - -// Test Protocol subclass for testing -class TestProtocolImpl extends Protocol { - protected assertCapabilityForMethod(): void {} - protected assertNotificationCapability(): void {} - protected assertRequestHandlerCapability(): void {} - protected buildContext(ctx: BaseContext): BaseContext { - return ctx; - } -} - -function createTestProtocol(): TestProtocolImpl { - return new TestProtocolImpl(); -} - -// Type helper for accessing private/protected Protocol properties in tests -interface TestProtocolInternals { - _responseHandlers: Map void>; -} - -// Mock Transport class -class MockTransport implements Transport { - onclose?: () => void; - onerror?: (error: Error) => void; - onmessage?: (message: unknown) => void; - - async start(): Promise {} - async close(): Promise { - this.onclose?.(); - } - async send(_message: JSONRPCMessage, _options?: TransportSendOptions): Promise {} -} - -/** - * Helper to call the protected _requestWithSchema method from tests that - * use custom method names not present in RequestMethod. - */ -// eslint-disable-next-line @typescript-eslint/no-explicit-any -function testRequest(proto: Protocol, request: Request, resultSchema: ZodType, options?: any) { - return ( - proto as unknown as { _requestWithSchema: (request: Request, resultSchema: ZodType, options?: unknown) => Promise } - )._requestWithSchema(request, resultSchema, options); -} - -describe('protocol tests', () => { - let protocol: Protocol; - let transport: MockTransport; - let sendSpy: MockInstance; - - beforeEach(() => { - transport = new MockTransport(); - sendSpy = vi.spyOn(transport, 'send'); - protocol = createTestProtocol(); - }); - - test('should throw a timeout error if the request exceeds the timeout', async () => { - await protocol.connect(transport); - const request = { method: 'example', params: {} }; - try { - const mockSchema: ZodType<{ result: string }> = z.object({ - result: z.string() - }); - await testRequest(protocol, request, mockSchema, { - timeout: 0 - }); - } catch (error) { - expect(error).toBeInstanceOf(SdkError); - if (error instanceof SdkError) { - expect(error.code).toBe(SdkErrorCode.RequestTimeout); - } - } - }); - - test('should invoke onclose when the connection is closed', async () => { - const oncloseMock = vi.fn(); - protocol.onclose = oncloseMock; - await protocol.connect(transport); - await transport.close(); - expect(oncloseMock).toHaveBeenCalled(); - }); - - test('should abort in-flight request handlers when the connection is closed', async () => { - await protocol.connect(transport); - - let abortReason: unknown; - let handlerStarted = false; - const handlerDone = new Promise(resolve => { - protocol.setRequestHandler('ping', async (_request, ctx) => { - handlerStarted = true; - await new Promise(resolveInner => { - ctx.mcpReq.signal.addEventListener('abort', () => { - abortReason = ctx.mcpReq.signal.reason; - resolveInner(); - }); - }); - resolve(); - return {}; - }); - }); - - transport.onmessage?.({ jsonrpc: '2.0', id: 1, method: 'ping', params: {} }); - - await vi.waitFor(() => expect(handlerStarted).toBe(true)); - - await transport.close(); - await handlerDone; - - expect(abortReason).toBeInstanceOf(SdkError); - expect((abortReason as SdkError).code).toBe(SdkErrorCode.ConnectionClosed); - }); - - test('should remove abort listener from caller signal when request settles', async () => { - await protocol.connect(transport); - - const controller = new AbortController(); - const addSpy = vi.spyOn(controller.signal, 'addEventListener'); - const removeSpy = vi.spyOn(controller.signal, 'removeEventListener'); - - const mockSchema = z.object({ result: z.string() }); - const reqPromise = testRequest(protocol, { method: 'example', params: {} }, mockSchema, { - signal: controller.signal - }); - - expect(addSpy).toHaveBeenCalledTimes(1); - const listener = addSpy.mock.calls[0]![1]; - - transport.onmessage?.({ jsonrpc: '2.0', id: 0, result: { result: 'ok' } }); - await reqPromise; - - expect(removeSpy).toHaveBeenCalledWith('abort', listener); - }); - - test('should not accumulate abort listeners when reusing a signal across requests', async () => { - await protocol.connect(transport); - - const controller = new AbortController(); - const addSpy = vi.spyOn(controller.signal, 'addEventListener'); - const removeSpy = vi.spyOn(controller.signal, 'removeEventListener'); - - const mockSchema = z.object({ result: z.string() }); - for (let i = 0; i < 5; i++) { - const reqPromise = testRequest(protocol, { method: 'example', params: {} }, mockSchema, { - signal: controller.signal - }); - transport.onmessage?.({ jsonrpc: '2.0', id: i, result: { result: 'ok' } }); - await reqPromise; - } - - expect(addSpy).toHaveBeenCalledTimes(5); - expect(removeSpy).toHaveBeenCalledTimes(5); - }); - - test('should remove abort listener when request rejects', async () => { - await protocol.connect(transport); - - const controller = new AbortController(); - const removeSpy = vi.spyOn(controller.signal, 'removeEventListener'); - - const mockSchema = z.object({ result: z.string() }); - await expect( - testRequest(protocol, { method: 'example', params: {} }, mockSchema, { - signal: controller.signal, - timeout: 0 - }) - ).rejects.toThrow(); - - expect(removeSpy).toHaveBeenCalledWith('abort', expect.any(Function)); - }); - - test('should not overwrite existing hooks when connecting transports', async () => { - const oncloseMock = vi.fn(); - const onerrorMock = vi.fn(); - const onmessageMock = vi.fn(); - transport.onclose = oncloseMock; - transport.onerror = onerrorMock; - transport.onmessage = onmessageMock; - await protocol.connect(transport); - transport.onclose(); - transport.onerror(new Error()); - transport.onmessage(''); - expect(oncloseMock).toHaveBeenCalled(); - expect(onerrorMock).toHaveBeenCalled(); - expect(onmessageMock).toHaveBeenCalled(); - }); - - describe('_meta preservation with onprogress', () => { - test('should preserve existing _meta when adding progressToken', async () => { - await protocol.connect(transport); - const request = { - method: 'example', - params: { - data: 'test', - _meta: { - customField: 'customValue', - anotherField: 123 - } - } - }; - const mockSchema: ZodType<{ result: string }> = z.object({ - result: z.string() - }); - const onProgressMock = vi.fn(); - - // Start request but don't await - we're testing the sent message - void testRequest(protocol, request, mockSchema, { - onprogress: onProgressMock - }).catch(() => { - // May not complete, ignore error - }); - - expect(sendSpy).toHaveBeenCalledWith( - expect.objectContaining({ - method: 'example', - params: { - data: 'test', - _meta: { - customField: 'customValue', - anotherField: 123, - progressToken: expect.any(Number) - } - }, - jsonrpc: '2.0', - id: expect.any(Number) - }), - expect.any(Object) - ); - }); - - test('should create _meta with progressToken when no _meta exists', async () => { - await protocol.connect(transport); - const request = { - method: 'example', - params: { - data: 'test' - } - }; - const mockSchema: ZodType<{ result: string }> = z.object({ - result: z.string() - }); - const onProgressMock = vi.fn(); - - // Start request but don't await - we're testing the sent message - void testRequest(protocol, request, mockSchema, { - onprogress: onProgressMock - }).catch(() => { - // May not complete, ignore error - }); - - expect(sendSpy).toHaveBeenCalledWith( - expect.objectContaining({ - method: 'example', - params: { - data: 'test', - _meta: { - progressToken: expect.any(Number) - } - }, - jsonrpc: '2.0', - id: expect.any(Number) - }), - expect.any(Object) - ); - }); - - test('should not modify _meta when onprogress is not provided', async () => { - await protocol.connect(transport); - const request = { - method: 'example', - params: { - data: 'test', - _meta: { - customField: 'customValue' - } - } - }; - const mockSchema: ZodType<{ result: string }> = z.object({ - result: z.string() - }); - - // Start request but don't await - we're testing the sent message - void testRequest(protocol, request, mockSchema).catch(() => { - // May not complete, ignore error - }); - - expect(sendSpy).toHaveBeenCalledWith( - expect.objectContaining({ - method: 'example', - params: { - data: 'test', - _meta: { - customField: 'customValue' - } - }, - jsonrpc: '2.0', - id: expect.any(Number) - }), - expect.any(Object) - ); - }); - - test('should handle params being undefined with onprogress', async () => { - await protocol.connect(transport); - const request = { - method: 'example' - }; - const mockSchema: ZodType<{ result: string }> = z.object({ - result: z.string() - }); - const onProgressMock = vi.fn(); - - // Start request but don't await - we're testing the sent message - void testRequest(protocol, request, mockSchema, { - onprogress: onProgressMock - }).catch(() => { - // May not complete, ignore error - }); - - expect(sendSpy).toHaveBeenCalledWith( - expect.objectContaining({ - method: 'example', - params: { - _meta: { - progressToken: expect.any(Number) - } - }, - jsonrpc: '2.0', - id: expect.any(Number) - }), - expect.any(Object) - ); - }); - }); - - describe('progress notification timeout behavior', () => { - beforeEach(() => { - vi.useFakeTimers(); - }); - afterEach(() => { - vi.useRealTimers(); - }); - - test('should not reset timeout when resetTimeoutOnProgress is false', async () => { - await protocol.connect(transport); - const request = { method: 'example', params: {} }; - const mockSchema: ZodType<{ result: string }> = z.object({ - result: z.string() - }); - const onProgressMock = vi.fn(); - const requestPromise = testRequest(protocol, request, mockSchema, { - timeout: 1000, - resetTimeoutOnProgress: false, - onprogress: onProgressMock - }); - - vi.advanceTimersByTime(800); - - if (transport.onmessage) { - transport.onmessage({ - jsonrpc: '2.0', - method: 'notifications/progress', - params: { - progressToken: 0, - progress: 50, - total: 100 - } - }); - } - await Promise.resolve(); - - expect(onProgressMock).toHaveBeenCalledWith({ - progress: 50, - total: 100 - }); - - vi.advanceTimersByTime(201); - - await expect(requestPromise).rejects.toThrow('Request timed out'); - }); - - test('should reset timeout when progress notification is received', async () => { - await protocol.connect(transport); - const request = { method: 'example', params: {} }; - const mockSchema: ZodType<{ result: string }> = z.object({ - result: z.string() - }); - const onProgressMock = vi.fn(); - const requestPromise = testRequest(protocol, request, mockSchema, { - timeout: 1000, - resetTimeoutOnProgress: true, - onprogress: onProgressMock - }); - vi.advanceTimersByTime(800); - if (transport.onmessage) { - transport.onmessage({ - jsonrpc: '2.0', - method: 'notifications/progress', - params: { - progressToken: 0, - progress: 50, - total: 100 - } - }); - } - await Promise.resolve(); - expect(onProgressMock).toHaveBeenCalledWith({ - progress: 50, - total: 100 - }); - vi.advanceTimersByTime(800); - if (transport.onmessage) { - transport.onmessage({ - jsonrpc: '2.0', - id: 0, - result: { result: 'success' } - }); - } - await Promise.resolve(); - await expect(requestPromise).resolves.toEqual({ result: 'success' }); - }); - - test('should respect maxTotalTimeout', async () => { - await protocol.connect(transport); - const request = { method: 'example', params: {} }; - const mockSchema: ZodType<{ result: string }> = z.object({ - result: z.string() - }); - const onProgressMock = vi.fn(); - const requestPromise = testRequest(protocol, request, mockSchema, { - timeout: 1000, - maxTotalTimeout: 150, - resetTimeoutOnProgress: true, - onprogress: onProgressMock - }); - - // First progress notification should work - vi.advanceTimersByTime(80); - if (transport.onmessage) { - transport.onmessage({ - jsonrpc: '2.0', - method: 'notifications/progress', - params: { - progressToken: 0, - progress: 50, - total: 100 - } - }); - } - await Promise.resolve(); - expect(onProgressMock).toHaveBeenCalledWith({ - progress: 50, - total: 100 - }); - vi.advanceTimersByTime(80); - if (transport.onmessage) { - transport.onmessage({ - jsonrpc: '2.0', - method: 'notifications/progress', - params: { - progressToken: 0, - progress: 75, - total: 100 - } - }); - } - await expect(requestPromise).rejects.toThrow('Maximum total timeout exceeded'); - expect(onProgressMock).toHaveBeenCalledTimes(1); - }); - - test('should timeout if no progress received within timeout period', async () => { - await protocol.connect(transport); - const request = { method: 'example', params: {} }; - const mockSchema: ZodType<{ result: string }> = z.object({ - result: z.string() - }); - const requestPromise = testRequest(protocol, request, mockSchema, { - timeout: 100, - resetTimeoutOnProgress: true - }); - vi.advanceTimersByTime(101); - await expect(requestPromise).rejects.toThrow('Request timed out'); - }); - - test('should handle multiple progress notifications correctly', async () => { - await protocol.connect(transport); - const request = { method: 'example', params: {} }; - const mockSchema: ZodType<{ result: string }> = z.object({ - result: z.string() - }); - const onProgressMock = vi.fn(); - const requestPromise = testRequest(protocol, request, mockSchema, { - timeout: 1000, - resetTimeoutOnProgress: true, - onprogress: onProgressMock - }); - - // Simulate multiple progress updates - for (let i = 1; i <= 3; i++) { - vi.advanceTimersByTime(800); - if (transport.onmessage) { - transport.onmessage({ - jsonrpc: '2.0', - method: 'notifications/progress', - params: { - progressToken: 0, - progress: i * 25, - total: 100 - } - }); - } - await Promise.resolve(); - expect(onProgressMock).toHaveBeenNthCalledWith(i, { - progress: i * 25, - total: 100 - }); - } - if (transport.onmessage) { - transport.onmessage({ - jsonrpc: '2.0', - id: 0, - result: { result: 'success' } - }); - } - await Promise.resolve(); - await expect(requestPromise).resolves.toEqual({ result: 'success' }); - }); - - test('should handle progress notifications with message field', async () => { - await protocol.connect(transport); - const request = { method: 'example', params: {} }; - const mockSchema: ZodType<{ result: string }> = z.object({ - result: z.string() - }); - const onProgressMock = vi.fn(); - - const requestPromise = testRequest(protocol, request, mockSchema, { - timeout: 1000, - onprogress: onProgressMock - }); - - vi.advanceTimersByTime(200); - - if (transport.onmessage) { - transport.onmessage({ - jsonrpc: '2.0', - method: 'notifications/progress', - params: { - progressToken: 0, - progress: 25, - total: 100, - message: 'Initializing process...' - } - }); - } - await Promise.resolve(); - - expect(onProgressMock).toHaveBeenCalledWith({ - progress: 25, - total: 100, - message: 'Initializing process...' - }); - - vi.advanceTimersByTime(200); - - if (transport.onmessage) { - transport.onmessage({ - jsonrpc: '2.0', - method: 'notifications/progress', - params: { - progressToken: 0, - progress: 75, - total: 100, - message: 'Processing data...' - } - }); - } - await Promise.resolve(); - - expect(onProgressMock).toHaveBeenCalledWith({ - progress: 75, - total: 100, - message: 'Processing data...' - }); - - if (transport.onmessage) { - transport.onmessage({ - jsonrpc: '2.0', - id: 0, - result: { result: 'success' } - }); - } - await Promise.resolve(); - await expect(requestPromise).resolves.toEqual({ result: 'success' }); - }); - }); - - describe('Debounced Notifications', () => { - // We need to flush the microtask queue to test the debouncing logic. - // This helper function does that. - const flushMicrotasks = () => new Promise(resolve => setImmediate(resolve)); - - it('should NOT debounce a notification that has parameters', async () => { - // ARRANGE - protocol = new TestProtocolImpl({ debouncedNotificationMethods: ['test/debounced_with_params'] }); - await protocol.connect(transport); - - // ACT - // These notifications are configured for debouncing but contain params, so they should be sent immediately. - await protocol.notification({ method: 'test/debounced_with_params', params: { data: 1 } }); - await protocol.notification({ method: 'test/debounced_with_params', params: { data: 2 } }); - - // ASSERT - // Both should have been sent immediately to avoid data loss. - expect(sendSpy).toHaveBeenCalledTimes(2); - expect(sendSpy).toHaveBeenCalledWith(expect.objectContaining({ params: { data: 1 } }), undefined); - expect(sendSpy).toHaveBeenCalledWith(expect.objectContaining({ params: { data: 2 } }), undefined); - }); - - it('should NOT debounce a notification that has a relatedRequestId', async () => { - // ARRANGE - protocol = new TestProtocolImpl({ debouncedNotificationMethods: ['test/debounced_with_options'] }); - await protocol.connect(transport); - - // ACT - await protocol.notification({ method: 'test/debounced_with_options' }, { relatedRequestId: 'req-1' }); - await protocol.notification({ method: 'test/debounced_with_options' }, { relatedRequestId: 'req-2' }); - - // ASSERT - expect(sendSpy).toHaveBeenCalledTimes(2); - expect(sendSpy).toHaveBeenCalledWith(expect.any(Object), { relatedRequestId: 'req-1' }); - expect(sendSpy).toHaveBeenCalledWith(expect.any(Object), { relatedRequestId: 'req-2' }); - }); - - it('should clear pending debounced notifications on connection close', async () => { - // ARRANGE - protocol = new TestProtocolImpl({ debouncedNotificationMethods: ['test/debounced'] }); - await protocol.connect(transport); - - // ACT - // Schedule a notification but don't flush the microtask queue. - protocol.notification({ method: 'test/debounced' }); - - // Close the connection. This should clear the pending set. - await protocol.close(); - - // Now, flush the microtask queue. - await flushMicrotasks(); - - // ASSERT - // The send should never have happened because the transport was cleared. - expect(sendSpy).not.toHaveBeenCalled(); - }); - - it('should debounce multiple synchronous calls when params property is omitted', async () => { - // ARRANGE - protocol = new TestProtocolImpl({ debouncedNotificationMethods: ['test/debounced'] }); - await protocol.connect(transport); - - // ACT - // This is the more idiomatic way to write a notification with no params. - protocol.notification({ method: 'test/debounced' }); - protocol.notification({ method: 'test/debounced' }); - protocol.notification({ method: 'test/debounced' }); - - expect(sendSpy).not.toHaveBeenCalled(); - await flushMicrotasks(); - - // ASSERT - expect(sendSpy).toHaveBeenCalledTimes(1); - // The final sent object might not even have the `params` key, which is fine. - // We can check that it was called and that the params are "falsy". - const sentNotification = sendSpy.mock.calls[0]![0]; - expect(sentNotification.method).toBe('test/debounced'); - expect(sentNotification.params).toBeUndefined(); - }); - - it('should debounce calls when params is explicitly undefined', async () => { - // ARRANGE - protocol = new TestProtocolImpl({ debouncedNotificationMethods: ['test/debounced'] }); - await protocol.connect(transport); - - // ACT - protocol.notification({ method: 'test/debounced', params: undefined }); - protocol.notification({ method: 'test/debounced', params: undefined }); - await flushMicrotasks(); - - // ASSERT - expect(sendSpy).toHaveBeenCalledTimes(1); - expect(sendSpy).toHaveBeenCalledWith( - expect.objectContaining({ - method: 'test/debounced', - params: undefined - }), - undefined - ); - }); - - it('should send non-debounced notifications immediately and multiple times', async () => { - // ARRANGE - protocol = new TestProtocolImpl({ debouncedNotificationMethods: ['test/debounced'] }); // Configure for a different method - await protocol.connect(transport); - - // ACT - // Call a non-debounced notification method multiple times. - await protocol.notification({ method: 'test/immediate' }); - await protocol.notification({ method: 'test/immediate' }); - - // ASSERT - // Since this method is not in the debounce list, it should be sent every time. - expect(sendSpy).toHaveBeenCalledTimes(2); - }); - - it('should not debounce any notifications if the option is not provided', async () => { - // ARRANGE - // Use the default protocol from beforeEach, which has no debounce options. - await protocol.connect(transport); - - // ACT - await protocol.notification({ method: 'any/method' }); - await protocol.notification({ method: 'any/method' }); - - // ASSERT - // Without the config, behavior should be immediate sending. - expect(sendSpy).toHaveBeenCalledTimes(2); - }); - - it('should handle sequential batches of debounced notifications correctly', async () => { - // ARRANGE - protocol = new TestProtocolImpl({ debouncedNotificationMethods: ['test/debounced'] }); - await protocol.connect(transport); - - // ACT (Batch 1) - protocol.notification({ method: 'test/debounced' }); - protocol.notification({ method: 'test/debounced' }); - await flushMicrotasks(); - - // ASSERT (Batch 1) - expect(sendSpy).toHaveBeenCalledTimes(1); - - // ACT (Batch 2) - // After the first batch has been sent, a new batch should be possible. - protocol.notification({ method: 'test/debounced' }); - protocol.notification({ method: 'test/debounced' }); - await flushMicrotasks(); - - // ASSERT (Batch 2) - // The total number of sends should now be 2. - expect(sendSpy).toHaveBeenCalledTimes(2); - }); - }); - - describe('notifications/cancelled behavior', () => { - test('should abort request handler when notifications/cancelled is received', async () => { - await protocol.connect(transport); - - // Set up a request handler that checks if it was aborted - let wasAborted = false; - protocol.setRequestHandler('ping', async (_request, ctx) => { - // Simulate a long-running operation - await new Promise(resolve => setTimeout(resolve, 100)); - wasAborted = ctx.mcpReq.signal.aborted; - return {}; - }); - - // Simulate an incoming request - const requestId = 123; - if (transport.onmessage) { - transport.onmessage({ - jsonrpc: '2.0', - id: requestId, - method: 'ping', - params: {} - }); - } - - // Wait a bit for the handler to start - await new Promise(resolve => setTimeout(resolve, 10)); - - // Send cancellation notification - if (transport.onmessage) { - transport.onmessage({ - jsonrpc: '2.0', - method: 'notifications/cancelled', - params: { - requestId: requestId, - reason: 'User cancelled' - } - }); - } - - // Wait for the handler to complete - await new Promise(resolve => setTimeout(resolve, 150)); - - // Verify the request was aborted - expect(wasAborted).toBe(true); - }); - }); -}); - -// (2025-11 experimental test suites removed under SEP-2663; see git history.) -describe('mergeCapabilities', () => { - it('should merge client capabilities', () => { - const base: ClientCapabilities = { - sampling: {}, - roots: { - listChanged: true - } - }; - - const additional: ClientCapabilities = { - experimental: { - feature: { - featureFlag: true - } - }, - elicitation: {}, - roots: { - listChanged: true - } - }; - - const merged = mergeCapabilities(base, additional); - expect(merged).toEqual({ - sampling: {}, - elicitation: {}, - roots: { - listChanged: true - }, - experimental: { - feature: { - featureFlag: true - } - } - }); - }); - - it('should merge server capabilities', () => { - const base: ServerCapabilities = { - logging: {}, - prompts: { - listChanged: true - } - }; - - const additional: ServerCapabilities = { - resources: { - subscribe: true - }, - prompts: { - listChanged: true - } - }; - - const merged = mergeCapabilities(base, additional); - expect(merged).toEqual({ - logging: {}, - prompts: { - listChanged: true - }, - resources: { - subscribe: true - } - }); - }); - - it('should override existing values with additional values', () => { - const base: ServerCapabilities = { - prompts: { - listChanged: false - } - }; - - const additional: ServerCapabilities = { - prompts: { - listChanged: true - } - }; - - const merged = mergeCapabilities(base, additional); - expect(merged.prompts!.listChanged).toBe(true); - }); - - it('should handle empty objects', () => { - const base = {}; - const additional = {}; - const merged = mergeCapabilities(base, additional); - expect(merged).toEqual({}); - }); -}); diff --git a/packages/core/test/shared/protocolTransportHandling.test.ts b/packages/core/test/shared/protocolTransportHandling.test.ts deleted file mode 100644 index 731914265b..0000000000 --- a/packages/core/test/shared/protocolTransportHandling.test.ts +++ /dev/null @@ -1,123 +0,0 @@ -import { beforeEach, describe, expect, test } from 'vitest'; - -import type { BaseContext } from '../../src/shared/protocol'; -import { Protocol } from '../../src/shared/protocol'; -import type { Transport } from '../../src/shared/transport'; -import type { EmptyResult, JSONRPCMessage, Notification, Request, Result } from '../../src/types/index'; - -// Mock Transport class -class MockTransport implements Transport { - id: string; - onclose?: () => void; - onerror?: (error: Error) => void; - onmessage?: (message: unknown) => void; - sentMessages: JSONRPCMessage[] = []; - - constructor(id: string) { - this.id = id; - } - - async start(): Promise {} - - async close(): Promise { - this.onclose?.(); - } - - async send(message: JSONRPCMessage): Promise { - this.sentMessages.push(message); - } -} - -describe('Protocol transport handling bug', () => { - let protocol: Protocol; - let transportA: MockTransport; - let transportB: MockTransport; - - beforeEach(() => { - protocol = new (class extends Protocol { - protected assertCapabilityForMethod(): void {} - protected assertNotificationCapability(): void {} - protected assertRequestHandlerCapability(): void {} - protected buildContext(ctx: BaseContext): BaseContext { - return ctx; - } - })(); - - transportA = new MockTransport('A'); - transportB = new MockTransport('B'); - }); - - test('should send response to the correct transport when multiple clients are connected', async () => { - // Set up a request handler that simulates processing time - let resolveHandler: (value: EmptyResult) => void; - const handlerPromise = new Promise(resolve => { - resolveHandler = resolve; - }); - - protocol.setRequestHandler('ping', async () => handlerPromise); - - // Client A connects and sends a request - await protocol.connect(transportA); - transportA.onmessage?.({ jsonrpc: '2.0', method: 'ping', id: 1 }); - - // While A's request is being processed, client B connects - // This overwrites the transport reference in the protocol - await protocol.connect(transportB); - transportB.onmessage?.({ jsonrpc: '2.0', method: 'ping', id: 2 }); - - // Now complete A's request - resolveHandler!({}); - - // Wait for async operations to complete - await new Promise(resolve => setTimeout(resolve, 10)); - - // Check where the responses went - console.log('Transport A received:', transportA.sentMessages); - console.log('Transport B received:', transportB.sentMessages); - - // Transport A should receive response for request ID 1 - expect(transportA.sentMessages).toHaveLength(1); - expect(transportA.sentMessages[0]).toMatchObject({ jsonrpc: '2.0', id: 1, result: {} }); - - // Transport B should receive response for request ID 2 - expect(transportB.sentMessages).toHaveLength(1); - expect(transportB.sentMessages[0]).toMatchObject({ jsonrpc: '2.0', id: 2, result: {} }); - }); - - test('demonstrates the timing issue with multiple rapid connections', async () => { - const results: { transport: string; response: JSONRPCMessage[] }[] = []; - - // Set up handler with variable delay based on request id - protocol.setRequestHandler('ping', async (_request, ctx) => { - const delay = ctx.mcpReq.id === 1 ? 50 : 10; - await new Promise(resolve => setTimeout(resolve, delay)); - return {}; - }); - - // Rapid succession of connections and requests - await protocol.connect(transportA); - transportA.onmessage?.({ jsonrpc: '2.0', method: 'ping', id: 1 }); - - // Connect B while A is processing - setTimeout(async () => { - await protocol.connect(transportB); - transportB.onmessage?.({ jsonrpc: '2.0', method: 'ping', id: 2 }); - }, 10); - - // Wait for all processing - await new Promise(resolve => setTimeout(resolve, 100)); - - // Collect results - if (transportA.sentMessages.length > 0) { - results.push({ transport: 'A', response: transportA.sentMessages }); - } - if (transportB.sentMessages.length > 0) { - results.push({ transport: 'B', response: transportB.sentMessages }); - } - - console.log('Timing test results:', results); - - expect(transportA.sentMessages).toHaveLength(1); - expect(transportB.sentMessages).toHaveLength(1); - }); -}); diff --git a/packages/core/test/shared/stdio.test.ts b/packages/core/test/shared/stdio.test.ts deleted file mode 100644 index f8d27a4c1f..0000000000 --- a/packages/core/test/shared/stdio.test.ts +++ /dev/null @@ -1,158 +0,0 @@ -import { ReadBuffer, STDIO_DEFAULT_MAX_BUFFER_SIZE } from '../../src/shared/stdio'; -import type { JSONRPCMessage } from '../../src/types/index'; - -const testMessage: JSONRPCMessage = { - jsonrpc: '2.0', - method: 'foobar' -}; - -test('should have no messages after initialization', () => { - const readBuffer = new ReadBuffer(); - expect(readBuffer.readMessage()).toBeNull(); -}); - -test('should only yield a message after a newline', () => { - const readBuffer = new ReadBuffer(); - - readBuffer.append(Buffer.from(JSON.stringify(testMessage))); - expect(readBuffer.readMessage()).toBeNull(); - - readBuffer.append(Buffer.from('\n')); - expect(readBuffer.readMessage()).toEqual(testMessage); - expect(readBuffer.readMessage()).toBeNull(); -}); - -test('should be reusable after clearing', () => { - const readBuffer = new ReadBuffer(); - - readBuffer.append(Buffer.from('foobar')); - readBuffer.clear(); - expect(readBuffer.readMessage()).toBeNull(); - - readBuffer.append(Buffer.from(JSON.stringify(testMessage))); - readBuffer.append(Buffer.from('\n')); - expect(readBuffer.readMessage()).toEqual(testMessage); -}); - -describe('non-JSON line filtering', () => { - test('should skip empty lines', () => { - const readBuffer = new ReadBuffer(); - readBuffer.append(Buffer.from('\n\n' + JSON.stringify(testMessage) + '\n\n')); - - expect(readBuffer.readMessage()).toEqual(testMessage); - expect(readBuffer.readMessage()).toBeNull(); - }); - - test('should skip non-JSON lines before a valid message', () => { - const readBuffer = new ReadBuffer(); - readBuffer.append(Buffer.from('Debug: Starting server\n' + 'Warning: Something happened\n' + JSON.stringify(testMessage) + '\n')); - - expect(readBuffer.readMessage()).toEqual(testMessage); - expect(readBuffer.readMessage()).toBeNull(); - }); - - test('should skip non-JSON lines interleaved with multiple valid messages', () => { - const readBuffer = new ReadBuffer(); - const message1: JSONRPCMessage = { jsonrpc: '2.0', method: 'method1' }; - const message2: JSONRPCMessage = { jsonrpc: '2.0', method: 'method2' }; - - readBuffer.append( - Buffer.from( - 'Debug line 1\n' + - JSON.stringify(message1) + - '\n' + - 'Debug line 2\n' + - 'Another non-JSON line\n' + - JSON.stringify(message2) + - '\n' - ) - ); - - expect(readBuffer.readMessage()).toEqual(message1); - expect(readBuffer.readMessage()).toEqual(message2); - expect(readBuffer.readMessage()).toBeNull(); - }); - - test('should preserve incomplete JSON at end of buffer until completed', () => { - const readBuffer = new ReadBuffer(); - readBuffer.append(Buffer.from('{"jsonrpc": "2.0", "method": "test"')); - expect(readBuffer.readMessage()).toBeNull(); - - readBuffer.append(Buffer.from('}\n')); - expect(readBuffer.readMessage()).toEqual({ jsonrpc: '2.0', method: 'test' }); - }); - - test('should skip lines with unbalanced braces', () => { - const readBuffer = new ReadBuffer(); - readBuffer.append(Buffer.from('{incomplete\n' + 'incomplete}\n' + JSON.stringify(testMessage) + '\n')); - - expect(readBuffer.readMessage()).toEqual(testMessage); - expect(readBuffer.readMessage()).toBeNull(); - }); - - test('should skip lines that look like JSON but fail to parse', () => { - const readBuffer = new ReadBuffer(); - readBuffer.append(Buffer.from('{invalidJson: true}\n' + JSON.stringify(testMessage) + '\n')); - - expect(readBuffer.readMessage()).toEqual(testMessage); - expect(readBuffer.readMessage()).toBeNull(); - }); - - test('should tolerate leading/trailing whitespace around valid JSON', () => { - const readBuffer = new ReadBuffer(); - const message: JSONRPCMessage = { jsonrpc: '2.0', method: 'test' }; - readBuffer.append(Buffer.from(' ' + JSON.stringify(message) + ' \n')); - - expect(readBuffer.readMessage()).toEqual(message); - }); - - test('should still throw on valid JSON that fails schema validation', () => { - const readBuffer = new ReadBuffer(); - readBuffer.append(Buffer.from('{"not": "a jsonrpc message"}\n')); - - expect(() => readBuffer.readMessage()).toThrow(); - }); -}); - -describe('buffer size limit', () => { - test('should throw when buffer exceeds default max size', () => { - const readBuffer = new ReadBuffer(); - const chunkSize = 1024 * 1024; // 1 MB - const chunk = Buffer.alloc(chunkSize); - const chunksToFill = Math.floor(STDIO_DEFAULT_MAX_BUFFER_SIZE / chunkSize); - for (let i = 0; i < chunksToFill; i++) { - readBuffer.append(chunk); - } - expect(() => readBuffer.append(chunk)).toThrow(/ReadBuffer exceeded maximum size/); - }); - - test('should throw when buffer exceeds custom max size', () => { - const readBuffer = new ReadBuffer({ maxBufferSize: 100 }); - readBuffer.append(Buffer.alloc(50)); - expect(() => readBuffer.append(Buffer.alloc(51))).toThrow(/ReadBuffer exceeded maximum size/); - }); - - test('should clear buffer before throwing on overflow', () => { - const readBuffer = new ReadBuffer({ maxBufferSize: 100 }); - readBuffer.append(Buffer.alloc(50)); - expect(() => readBuffer.append(Buffer.alloc(51))).toThrow(); - - // Buffer should be cleared — can append again - readBuffer.append(Buffer.alloc(50)); - // And read messages normally - expect(readBuffer.readMessage()).toBeNull(); - }); - - test('should allow appending up to exactly the max size', () => { - const readBuffer = new ReadBuffer({ maxBufferSize: 100 }); - // Should not throw — exactly at limit - expect(() => readBuffer.append(Buffer.alloc(100))).not.toThrow(); - }); - - test('should work with no options (backwards compatible)', () => { - const readBuffer = new ReadBuffer(); - // Small append should always work - readBuffer.append(Buffer.from(JSON.stringify({ jsonrpc: '2.0', method: 'ping' }) + '\n')); - expect(readBuffer.readMessage()).not.toBeNull(); - }); -}); diff --git a/packages/core/test/shared/toolNameValidation.test.ts b/packages/core/test/shared/toolNameValidation.test.ts deleted file mode 100644 index 5628b731ca..0000000000 --- a/packages/core/test/shared/toolNameValidation.test.ts +++ /dev/null @@ -1,130 +0,0 @@ -import type { MockInstance } from 'vitest'; -import { vi } from 'vitest'; - -import { issueToolNameWarning, validateAndWarnToolName, validateToolName } from '../../src/shared/toolNameValidation'; - -// Spy on console.warn to capture output -let warnSpy: MockInstance; - -beforeEach(() => { - warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); -}); - -afterEach(() => { - vi.restoreAllMocks(); -}); - -describe('validateToolName', () => { - describe('valid tool names', () => { - test.each` - description | toolName - ${'simple alphanumeric names'} | ${'getUser'} - ${'names with underscores'} | ${'get_user_profile'} - ${'names with dashes'} | ${'user-profile-update'} - ${'names with dots'} | ${'admin.tools.list'} - ${'mixed character names'} | ${'DATA_EXPORT_v2.1'} - ${'single character names'} | ${'a'} - ${'128 character names'} | ${'a'.repeat(128)} - `('should accept $description', ({ toolName }) => { - const result = validateToolName(toolName); - expect(result.isValid).toBe(true); - expect(result.warnings).toHaveLength(0); - }); - }); - - describe('invalid tool names', () => { - test.each` - description | toolName | expectedWarning - ${'empty names'} | ${''} | ${'Tool name cannot be empty'} - ${'names longer than 128 characters'} | ${'a'.repeat(129)} | ${'Tool name exceeds maximum length of 128 characters (current: 129)'} - ${'names with spaces'} | ${'get user profile'} | ${'Tool name contains invalid characters: " "'} - ${'names with commas'} | ${'get,user,profile'} | ${'Tool name contains invalid characters: ","'} - ${'names with forward slashes'} | ${'user/profile/update'} | ${'Tool name contains invalid characters: "/"'} - ${'names with other special chars'} | ${'user@domain.com'} | ${'Tool name contains invalid characters: "@"'} - ${'names with multiple invalid chars'} | ${'user name@domain,com'} | ${'Tool name contains invalid characters: " ", "@", ","'} - ${'names with unicode characters'} | ${'user-ñame'} | ${'Tool name contains invalid characters: "ñ"'} - `('should reject $description', ({ toolName, expectedWarning }) => { - const result = validateToolName(toolName); - expect(result.isValid).toBe(false); - expect(result.warnings).toContain(expectedWarning); - }); - }); - - describe('warnings for potentially problematic patterns', () => { - test.each` - description | toolName | expectedWarning | shouldBeValid - ${'names with spaces'} | ${'get user profile'} | ${'Tool name contains spaces, which may cause parsing issues'} | ${false} - ${'names with commas'} | ${'get,user,profile'} | ${'Tool name contains commas, which may cause parsing issues'} | ${false} - ${'names starting with dash'} | ${'-get-user'} | ${'Tool name starts or ends with a dash, which may cause parsing issues in some contexts'} | ${true} - ${'names ending with dash'} | ${'get-user-'} | ${'Tool name starts or ends with a dash, which may cause parsing issues in some contexts'} | ${true} - ${'names starting with dot'} | ${'.get.user'} | ${'Tool name starts or ends with a dot, which may cause parsing issues in some contexts'} | ${true} - ${'names ending with dot'} | ${'get.user.'} | ${'Tool name starts or ends with a dot, which may cause parsing issues in some contexts'} | ${true} - ${'names with leading and trailing dots'} | ${'.get.user.'} | ${'Tool name starts or ends with a dot, which may cause parsing issues in some contexts'} | ${true} - `('should warn about $description', ({ toolName, expectedWarning, shouldBeValid }) => { - const result = validateToolName(toolName); - expect(result.isValid).toBe(shouldBeValid); - expect(result.warnings).toContain(expectedWarning); - }); - }); -}); - -describe('issueToolNameWarning', () => { - test('should output warnings to console.warn', () => { - const warnings = ['Warning 1', 'Warning 2']; - issueToolNameWarning('test-tool', warnings); - - expect(warnSpy).toHaveBeenCalledTimes(6); // Header + 2 warnings + 3 guidance lines - const calls = warnSpy.mock.calls.map(call => call.join(' ')); - expect(calls[0]).toContain('Tool name validation warning for "test-tool"'); - expect(calls[1]).toContain('- Warning 1'); - expect(calls[2]).toContain('- Warning 2'); - expect(calls[3]).toContain('Tool registration will proceed, but this may cause compatibility issues.'); - expect(calls[4]).toContain('Consider updating the tool name'); - expect(calls[5]).toContain('See SEP: Specify Format for Tool Names'); - }); - - test('should handle empty warnings array', () => { - issueToolNameWarning('test-tool', []); - expect(warnSpy).toHaveBeenCalledTimes(0); - }); -}); - -describe('validateAndWarnToolName', () => { - test.each` - description | toolName | expectedResult | shouldWarn - ${'valid names with warnings'} | ${'-get-user-'} | ${true} | ${true} - ${'completely valid names'} | ${'get-user-profile'} | ${true} | ${false} - ${'invalid names with spaces'} | ${'get user profile'} | ${false} | ${true} - ${'empty names'} | ${''} | ${false} | ${true} - ${'names exceeding length limit'} | ${'a'.repeat(129)} | ${false} | ${true} - `('should handle $description', ({ toolName, expectedResult, shouldWarn }) => { - const result = validateAndWarnToolName(toolName); - expect(result).toBe(expectedResult); - - if (shouldWarn) { - expect(warnSpy).toHaveBeenCalled(); - } else { - expect(warnSpy).not.toHaveBeenCalled(); - } - }); - - test('should include space warning for invalid names with spaces', () => { - validateAndWarnToolName('get user profile'); - const warningCalls = warnSpy.mock.calls.map(call => call.join(' ')); - expect(warningCalls.some(call => call.includes('Tool name contains spaces'))).toBe(true); - }); -}); - -describe('edge cases and robustness', () => { - test.each` - description | toolName | shouldBeValid | expectedWarning - ${'names with only dots'} | ${'...'} | ${true} | ${'Tool name starts or ends with a dot, which may cause parsing issues in some contexts'} - ${'names with only dashes'} | ${'---'} | ${true} | ${'Tool name starts or ends with a dash, which may cause parsing issues in some contexts'} - ${'names with only forward slashes'} | ${'///'} | ${false} | ${'Tool name contains invalid characters: "/"'} - ${'names with mixed valid/invalid chars'} | ${'user@name123'} | ${false} | ${'Tool name contains invalid characters: "@"'} - `('should handle $description', ({ toolName, shouldBeValid, expectedWarning }) => { - const result = validateToolName(toolName); - expect(result.isValid).toBe(shouldBeValid); - expect(result.warnings).toContain(expectedWarning); - }); -}); diff --git a/packages/core/test/shared/traceContextMeta.test.ts b/packages/core/test/shared/traceContextMeta.test.ts deleted file mode 100644 index 312fbca6e3..0000000000 --- a/packages/core/test/shared/traceContextMeta.test.ts +++ /dev/null @@ -1,120 +0,0 @@ -import { describe, expect, it } from 'vitest'; -import { z } from 'zod/v4'; - -import { Protocol } from '../../src/shared/protocol'; -import type { BaseContext } from '../../src/exports/public/index'; -import { BAGGAGE_META_KEY, TRACEPARENT_META_KEY, TRACESTATE_META_KEY } from '../../src/exports/public/index'; -import { InMemoryTransport } from '../../src/util/inMemory'; - -class TestProtocol extends Protocol { - protected buildContext(ctx: BaseContext): BaseContext { - return ctx; - } - protected assertCapabilityForMethod(): void {} - protected assertNotificationCapability(): void {} - protected assertRequestHandlerCapability(): void {} -} - -async function pair(): Promise<[TestProtocol, TestProtocol]> { - const [t1, t2] = InMemoryTransport.createLinkedPair(); - const a = new TestProtocol(); - const b = new TestProtocol(); - await a.connect(t1); - await b.connect(t2); - return [a, b]; -} - -const TRACEPARENT = '00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01'; -const TRACESTATE = 'vendor1=opaqueValue1,vendor2=opaqueValue2'; -const BAGGAGE = 'userId=alice,serverRegion=us-east-1'; - -describe('SEP-414 trace context `_meta` passthrough', () => { - it('exposes reserved unprefixed key names', () => { - // SEP-414 reserves these exact unprefixed keys as an exception to the - // `_meta` prefix rule; a drifted constant would break interop. - expect(TRACEPARENT_META_KEY).toBe('traceparent'); - expect(TRACESTATE_META_KEY).toBe('tracestate'); - expect(BAGGAGE_META_KEY).toBe('baggage'); - }); - - it('passes request `_meta` trace context through to the server-side handler untouched', async () => { - const [a, b] = await pair(); - let seenMeta: Record | undefined; - b.setRequestHandler('acme/traced', { params: z.object({ v: z.string() }) }, async (params, ctx) => { - seenMeta = ctx.mcpReq._meta; - return { echoed: params.v }; - }); - - await a.request( - { - method: 'acme/traced', - params: { - v: 'x', - _meta: { - [TRACEPARENT_META_KEY]: TRACEPARENT, - [TRACESTATE_META_KEY]: TRACESTATE, - [BAGGAGE_META_KEY]: BAGGAGE - } - } - }, - z.object({ echoed: z.string() }) - ); - - expect(seenMeta).toMatchObject({ - [TRACEPARENT_META_KEY]: TRACEPARENT, - [TRACESTATE_META_KEY]: TRACESTATE, - [BAGGAGE_META_KEY]: BAGGAGE - }); - }); - - it('passes response `_meta` trace context back to the requester untouched', async () => { - const [a, b] = await pair(); - b.setRequestHandler('acme/traced', { params: z.object({}) }, async (_params, ctx) => ({ - ok: true, - _meta: { - // Echo the inbound trace context onto the response envelope. - ...ctx.mcpReq._meta - } - })); - - const result = await a.request( - { - method: 'acme/traced', - params: { - _meta: { - [TRACEPARENT_META_KEY]: TRACEPARENT, - [TRACESTATE_META_KEY]: TRACESTATE, - [BAGGAGE_META_KEY]: BAGGAGE - } - } - }, - z.object({ ok: z.boolean(), _meta: z.record(z.string(), z.unknown()).optional() }) - ); - - expect(result.ok).toBe(true); - expect(result._meta).toMatchObject({ - [TRACEPARENT_META_KEY]: TRACEPARENT, - [TRACESTATE_META_KEY]: TRACESTATE, - [BAGGAGE_META_KEY]: BAGGAGE - }); - }); - - it('passes notification `_meta` trace context through to the handler', async () => { - const [a, b] = await pair(); - let seenMeta: unknown; - b.setNotificationHandler('acme/tracedEvent', { params: z.object({ stage: z.string() }) }, (_params, notification) => { - seenMeta = notification.params?._meta; - }); - - await a.notification({ - method: 'acme/tracedEvent', - params: { - stage: 'fetch', - _meta: { [TRACEPARENT_META_KEY]: TRACEPARENT, [BAGGAGE_META_KEY]: BAGGAGE } - } - }); - await new Promise(r => setTimeout(r, 0)); - - expect(seenMeta).toEqual({ [TRACEPARENT_META_KEY]: TRACEPARENT, [BAGGAGE_META_KEY]: BAGGAGE }); - }); -}); diff --git a/packages/core/test/shared/transport.test.ts b/packages/core/test/shared/transport.test.ts deleted file mode 100644 index 2a17ac0643..0000000000 --- a/packages/core/test/shared/transport.test.ts +++ /dev/null @@ -1,182 +0,0 @@ -import { createFetchWithInit, type FetchLike, normalizeHeaders } from '../../src/shared/transport'; - -describe('normalizeHeaders', () => { - test('returns empty object for undefined', () => { - expect(normalizeHeaders(undefined)).toEqual({}); - }); - - test('handles Headers instance', () => { - const headers = new Headers({ - 'x-foo': 'bar', - 'content-type': 'application/json' - }); - expect(normalizeHeaders(headers)).toEqual({ - 'x-foo': 'bar', - 'content-type': 'application/json' - }); - }); - - test('handles array of tuples', () => { - const headers: [string, string][] = [ - ['x-foo', 'bar'], - ['x-baz', 'qux'] - ]; - expect(normalizeHeaders(headers)).toEqual({ - 'x-foo': 'bar', - 'x-baz': 'qux' - }); - }); - - test('handles plain object', () => { - const headers = { 'x-foo': 'bar', 'x-baz': 'qux' }; - expect(normalizeHeaders(headers)).toEqual({ - 'x-foo': 'bar', - 'x-baz': 'qux' - }); - }); - - test('returns a shallow copy for plain objects', () => { - const headers = { 'x-foo': 'bar' }; - const result = normalizeHeaders(headers); - expect(result).not.toBe(headers); - expect(result).toEqual(headers); - }); -}); - -describe('createFetchWithInit', () => { - test('returns baseFetch unchanged when no baseInit provided', () => { - const mockFetch: FetchLike = vi.fn(); - const result = createFetchWithInit(mockFetch); - expect(result).toBe(mockFetch); - }); - - test('passes baseInit to fetch when no call init provided', async () => { - const mockFetch: FetchLike = vi.fn(); - const baseInit: RequestInit = { - method: 'POST', - credentials: 'include' - }; - - const wrappedFetch = createFetchWithInit(mockFetch, baseInit); - await wrappedFetch('https://example.com'); - - expect(mockFetch).toHaveBeenCalledWith( - 'https://example.com', - expect.objectContaining({ - method: 'POST', - credentials: 'include' - }) - ); - }); - - test('merges baseInit with call init, call init wins for non-header fields', async () => { - const mockFetch: FetchLike = vi.fn(); - const baseInit: RequestInit = { - method: 'POST', - credentials: 'include' - }; - - const wrappedFetch = createFetchWithInit(mockFetch, baseInit); - await wrappedFetch('https://example.com', { method: 'PUT' }); - - expect(mockFetch).toHaveBeenCalledWith( - 'https://example.com', - expect.objectContaining({ - method: 'PUT', - credentials: 'include' - }) - ); - }); - - test('merges headers from both base and call init', async () => { - const mockFetch: FetchLike = vi.fn(); - const baseInit: RequestInit = { - headers: { 'x-base': 'base-value', 'x-shared': 'base' } - }; - - const wrappedFetch = createFetchWithInit(mockFetch, baseInit); - await wrappedFetch('https://example.com', { - headers: { 'x-call': 'call-value', 'x-shared': 'call' } - }); - - expect(mockFetch).toHaveBeenCalledWith( - 'https://example.com', - expect.objectContaining({ - headers: { - 'x-base': 'base-value', - 'x-call': 'call-value', - 'x-shared': 'call' - } - }) - ); - }); - - test('uses baseInit headers when call init has no headers', async () => { - const mockFetch: FetchLike = vi.fn(); - const baseInit: RequestInit = { - headers: { 'x-base': 'base-value' } - }; - - const wrappedFetch = createFetchWithInit(mockFetch, baseInit); - await wrappedFetch('https://example.com', { method: 'POST' }); - - expect(mockFetch).toHaveBeenCalledWith( - 'https://example.com', - expect.objectContaining({ - method: 'POST', - headers: { 'x-base': 'base-value' } - }) - ); - }); - - test('handles URL object as first argument', async () => { - const mockFetch: FetchLike = vi.fn(); - const baseInit: RequestInit = { method: 'GET' }; - - const wrappedFetch = createFetchWithInit(mockFetch, baseInit); - const url = new URL('https://example.com/path'); - await wrappedFetch(url); - - expect(mockFetch).toHaveBeenCalledWith(url, expect.objectContaining({ method: 'GET' })); - }); - - test('passes all baseInit properties when call init is empty object', async () => { - const mockFetch: FetchLike = vi.fn(); - const baseInit: RequestInit = { - method: 'POST', - credentials: 'include', - headers: { 'x-base': 'value' } - }; - - const wrappedFetch = createFetchWithInit(mockFetch, baseInit); - await wrappedFetch('https://example.com', {}); - - expect(mockFetch).toHaveBeenCalledWith( - 'https://example.com', - expect.objectContaining({ - method: 'POST', - credentials: 'include', - headers: { 'x-base': 'value' } - }) - ); - }); - - test('passes Headers instance through when call init has no headers', async () => { - const mockFetch: FetchLike = vi.fn(); - const baseHeaders = new Headers({ 'x-base': 'value' }); - const baseInit: RequestInit = { - headers: baseHeaders - }; - - const wrappedFetch = createFetchWithInit(mockFetch, baseInit); - await wrappedFetch('https://example.com', { method: 'POST' }); - - expect(mockFetch).toHaveBeenCalledWith( - 'https://example.com', - expect.objectContaining({ - method: 'POST', - headers: baseHeaders - }) - ); - }); -}); diff --git a/packages/core/test/shared/uriTemplate.test.ts b/packages/core/test/shared/uriTemplate.test.ts deleted file mode 100644 index bfc3237872..0000000000 --- a/packages/core/test/shared/uriTemplate.test.ts +++ /dev/null @@ -1,314 +0,0 @@ -import { UriTemplate } from '../../src/shared/uriTemplate'; - -describe('UriTemplate', () => { - describe('isTemplate', () => { - it('should return true for strings containing template expressions', () => { - expect(UriTemplate.isTemplate('{foo}')).toBe(true); - expect(UriTemplate.isTemplate('/users/{id}')).toBe(true); - expect(UriTemplate.isTemplate('http://example.com/{path}/{file}')).toBe(true); - expect(UriTemplate.isTemplate('/search{?q,limit}')).toBe(true); - }); - - it('should return false for strings without template expressions', () => { - expect(UriTemplate.isTemplate('')).toBe(false); - expect(UriTemplate.isTemplate('plain string')).toBe(false); - expect(UriTemplate.isTemplate('http://example.com/foo/bar')).toBe(false); - expect(UriTemplate.isTemplate('{}')).toBe(false); // Empty braces don't count - expect(UriTemplate.isTemplate('{ }')).toBe(false); // Just whitespace doesn't count - }); - }); - - describe('simple string expansion', () => { - it('should expand simple string variables', () => { - const template = new UriTemplate('http://example.com/users/{username}'); - expect(template.expand({ username: 'fred' })).toBe('http://example.com/users/fred'); - expect(template.variableNames).toEqual(['username']); - }); - - it('should handle multiple variables', () => { - const template = new UriTemplate('{x,y}'); - expect(template.expand({ x: '1024', y: '768' })).toBe('1024,768'); - expect(template.variableNames).toEqual(['x', 'y']); - }); - - it('should encode reserved characters', () => { - const template = new UriTemplate('{var}'); - expect(template.expand({ var: 'value with spaces' })).toBe('value%20with%20spaces'); - }); - }); - - describe('reserved expansion', () => { - it('should not encode reserved characters with + operator', () => { - const template = new UriTemplate('{+path}/here'); - expect(template.expand({ path: '/foo/bar' })).toBe('/foo/bar/here'); - expect(template.variableNames).toEqual(['path']); - }); - }); - - describe('fragment expansion', () => { - it('should add # prefix and not encode reserved chars', () => { - const template = new UriTemplate('X{#var}'); - expect(template.expand({ var: '/test' })).toBe('X#/test'); - expect(template.variableNames).toEqual(['var']); - }); - }); - - describe('label expansion', () => { - it('should add . prefix', () => { - const template = new UriTemplate('X{.var}'); - expect(template.expand({ var: 'test' })).toBe('X.test'); - expect(template.variableNames).toEqual(['var']); - }); - }); - - describe('path expansion', () => { - it('should add / prefix', () => { - const template = new UriTemplate('X{/var}'); - expect(template.expand({ var: 'test' })).toBe('X/test'); - expect(template.variableNames).toEqual(['var']); - }); - }); - - describe('query expansion', () => { - it('should add ? prefix and name=value format', () => { - const template = new UriTemplate('X{?var}'); - expect(template.expand({ var: 'test' })).toBe('X?var=test'); - expect(template.variableNames).toEqual(['var']); - }); - }); - - describe('form continuation expansion', () => { - it('should add & prefix and name=value format', () => { - const template = new UriTemplate('X{&var}'); - expect(template.expand({ var: 'test' })).toBe('X&var=test'); - expect(template.variableNames).toEqual(['var']); - }); - }); - - describe('matching', () => { - it('should match simple strings and extract variables', () => { - const template = new UriTemplate('http://example.com/users/{username}'); - const match = template.match('http://example.com/users/fred'); - expect(match).toEqual({ username: 'fred' }); - }); - - it('should match multiple variables', () => { - const template = new UriTemplate('/users/{username}/posts/{postId}'); - const match = template.match('/users/fred/posts/123'); - expect(match).toEqual({ username: 'fred', postId: '123' }); - }); - - it('should return null for non-matching URIs', () => { - const template = new UriTemplate('/users/{username}'); - const match = template.match('/posts/123'); - expect(match).toBeNull(); - }); - - it('should handle exploded arrays', () => { - const template = new UriTemplate('{/list*}'); - const match = template.match('/red,green,blue'); - expect(match).toEqual({ list: ['red', 'green', 'blue'] }); - }); - }); - - describe('edge cases', () => { - it('should handle empty variables', () => { - const template = new UriTemplate('{empty}'); - expect(template.expand({})).toBe(''); - expect(template.expand({ empty: '' })).toBe(''); - }); - - it('should handle undefined variables', () => { - const template = new UriTemplate('{a}{b}{c}'); - expect(template.expand({ b: '2' })).toBe('2'); - }); - - it('should handle special characters in variable names', () => { - const template = new UriTemplate('{$var_name}'); - expect(template.expand({ $var_name: 'value' })).toBe('value'); - }); - }); - - describe('complex patterns', () => { - it('should handle nested path segments', () => { - const template = new UriTemplate('/api/{version}/{resource}/{id}'); - expect( - template.expand({ - version: 'v1', - resource: 'users', - id: '123' - }) - ).toBe('/api/v1/users/123'); - expect(template.variableNames).toEqual(['version', 'resource', 'id']); - }); - - it('should handle query parameters with arrays', () => { - const template = new UriTemplate('/search{?tags*}'); - expect( - template.expand({ - tags: ['nodejs', 'typescript', 'testing'] - }) - ).toBe('/search?tags=nodejs,typescript,testing'); - expect(template.variableNames).toEqual(['tags']); - }); - - it('should handle multiple query parameters', () => { - const template = new UriTemplate('/search{?q,page,limit}'); - expect( - template.expand({ - q: 'test', - page: '1', - limit: '10' - }) - ).toBe('/search?q=test&page=1&limit=10'); - expect(template.variableNames).toEqual(['q', 'page', 'limit']); - }); - }); - - describe('matching complex patterns', () => { - it('should match nested path segments', () => { - const template = new UriTemplate('/api/{version}/{resource}/{id}'); - const match = template.match('/api/v1/users/123'); - expect(match).toEqual({ - version: 'v1', - resource: 'users', - id: '123' - }); - expect(template.variableNames).toEqual(['version', 'resource', 'id']); - }); - - it('should match query parameters', () => { - const template = new UriTemplate('/search{?q}'); - const match = template.match('/search?q=test'); - expect(match).toEqual({ q: 'test' }); - expect(template.variableNames).toEqual(['q']); - }); - - it('should match multiple query parameters', () => { - const template = new UriTemplate('/search{?q,page}'); - const match = template.match('/search?q=test&page=1'); - expect(match).toEqual({ q: 'test', page: '1' }); - expect(template.variableNames).toEqual(['q', 'page']); - }); - - it('should handle partial matches correctly', () => { - const template = new UriTemplate('/users/{id}'); - expect(template.match('/users/123/extra')).toBeNull(); - expect(template.match('/users')).toBeNull(); - }); - }); - - describe('security and edge cases', () => { - it('should handle extremely long input strings', () => { - const longString = 'x'.repeat(100_000); - const template = new UriTemplate(`/api/{param}`); - expect(template.expand({ param: longString })).toBe(`/api/${longString}`); - expect(template.match(`/api/${longString}`)).toEqual({ param: longString }); - }); - - it('should handle deeply nested template expressions', () => { - const template = new UriTemplate('{a}{b}{c}{d}{e}{f}{g}{h}{i}{j}'.repeat(1000)); - expect(() => - template.expand({ - a: '1', - b: '2', - c: '3', - d: '4', - e: '5', - f: '6', - g: '7', - h: '8', - i: '9', - j: '0' - }) - ).not.toThrow(); - }); - - it('should handle malformed template expressions', () => { - expect(() => new UriTemplate('{unclosed')).toThrow(); - expect(() => new UriTemplate('{}')).not.toThrow(); - expect(() => new UriTemplate('{,}')).not.toThrow(); - expect(() => new UriTemplate('{a}{')).toThrow(); - }); - - it('should handle pathological regex patterns', () => { - const template = new UriTemplate('/api/{param}'); - // Create a string that could cause catastrophic backtracking - const input = '/api/' + 'a'.repeat(100_000); - expect(() => template.match(input)).not.toThrow(); - }); - - it('should handle invalid UTF-8 sequences', () => { - const template = new UriTemplate('/api/{param}'); - const invalidUtf8 = '���'; - expect(() => template.expand({ param: invalidUtf8 })).not.toThrow(); - expect(() => template.match(`/api/${invalidUtf8}`)).not.toThrow(); - }); - - it('should handle template/URI length mismatches', () => { - const template = new UriTemplate('/api/{param}'); - expect(template.match('/api/')).toBeNull(); - expect(template.match('/api')).toBeNull(); - expect(template.match('/api/value/extra')).toBeNull(); - }); - - it('should handle repeated operators', () => { - const template = new UriTemplate('{?a}{?b}{?c}'); - expect(template.expand({ a: '1', b: '2', c: '3' })).toBe('?a=1&b=2&c=3'); - expect(template.variableNames).toEqual(['a', 'b', 'c']); - }); - - it('should handle overlapping variable names', () => { - const template = new UriTemplate('{var}{vara}'); - expect(template.expand({ var: '1', vara: '2' })).toBe('12'); - expect(template.variableNames).toEqual(['var', 'vara']); - }); - - it('should handle empty segments', () => { - const template = new UriTemplate('///{a}////{b}////'); - expect(template.expand({ a: '1', b: '2' })).toBe('///1////2////'); - expect(template.match('///1////2////')).toEqual({ a: '1', b: '2' }); - expect(template.variableNames).toEqual(['a', 'b']); - }); - - it('should handle maximum template expression limit', () => { - // Create a template with many expressions - const expressions = Array.from({ length: 10_000 }).fill('{param}').join(''); - expect(() => new UriTemplate(expressions)).not.toThrow(); - }); - - it('should handle maximum variable name length', () => { - const longName = 'a'.repeat(10_000); - const template = new UriTemplate(`{${longName}}`); - const vars: Record = { [longName]: 'value' }; - expect(() => template.expand(vars)).not.toThrow(); - }); - - it('should not be vulnerable to ReDoS with exploded path patterns', () => { - // Test for ReDoS vulnerability (CVE-2026-0621) - // See: https://github.com/modelcontextprotocol/typescript-sdk/issues/965 - const template = new UriTemplate('{/id*}'); - const maliciousPayload = '/' + ','.repeat(50); - - const startTime = Date.now(); - template.match(maliciousPayload); - const elapsed = Date.now() - startTime; - - // Should complete in under 100ms, not hang for seconds - expect(elapsed).toBeLessThan(100); - }); - - it('should not be vulnerable to ReDoS with exploded simple patterns', () => { - // Test for ReDoS vulnerability with simple exploded operator - const template = new UriTemplate('{id*}'); - const maliciousPayload = ','.repeat(50); - - const startTime = Date.now(); - template.match(maliciousPayload); - const elapsed = Date.now() - startTime; - - // Should complete in under 100ms, not hang for seconds - expect(elapsed).toBeLessThan(100); - }); - }); -}); diff --git a/packages/core/test/shared/wrapHandler.test.ts b/packages/core/test/shared/wrapHandler.test.ts deleted file mode 100644 index 06a5045ecd..0000000000 --- a/packages/core/test/shared/wrapHandler.test.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { describe, expect, it } from 'vitest'; - -import { Protocol } from '../../src/shared/protocol'; -import type { BaseContext, JSONRPCRequest, Result } from '../../src/exports/public/index'; - -class TestProtocol extends Protocol { - protected buildContext(ctx: BaseContext): BaseContext { - return ctx; - } - protected assertCapabilityForMethod(): void {} - protected assertNotificationCapability(): void {} - protected assertRequestHandlerCapability(): void {} -} - -describe('Protocol._wrapHandler', () => { - it('routes setRequestHandler registration through _wrapHandler', () => { - const seen: string[] = []; - class SpyProtocol extends TestProtocol { - protected override _wrapHandler( - method: string, - handler: (request: JSONRPCRequest, ctx: BaseContext) => Promise - ): (request: JSONRPCRequest, ctx: BaseContext) => Promise { - seen.push(method); - return handler; - } - } - const p = new SpyProtocol(); - seen.length = 0; - p.setRequestHandler('tools/list', () => ({ tools: [] })); - p.setRequestHandler('resources/list', () => ({ resources: [] })); - expect(seen).toEqual(['tools/list', 'resources/list']); - }); -}); diff --git a/packages/core/test/spec.types.2025-11-25.test.ts b/packages/core/test/spec.types.2025-11-25.test.ts deleted file mode 100644 index ad3fec3f92..0000000000 --- a/packages/core/test/spec.types.2025-11-25.test.ts +++ /dev/null @@ -1,950 +0,0 @@ -/** - * Compares the SDK's types against the frozen 2025-11-25 release schema - * (spec.types.2025-11-25.ts). The 2026-07-28 comparison lives in - * spec.types.2026-07-28.test.ts. - * - * This contains: - * - Static type checks to verify the Spec's types are compatible with the SDK's types - * (mutually assignable — no type-level workarounds should be needed) - * - Runtime checks to verify each Spec type has a static check - * (note: a few don't have SDK types, see MISSING_SDK_TYPES below) - */ -import fs from 'node:fs'; -import path from 'node:path'; - -import type * as SpecTypes from '../src/types/spec.types.2025-11-25'; -import type * as SDKTypes from '../src/types/index'; - -/* eslint-disable @typescript-eslint/no-unused-vars */ - -// Adds the `jsonrpc` property to a type, to match the on-wire format of notifications. -type WithJSONRPC = T & { jsonrpc: '2.0' }; - -// Adds the `jsonrpc` and `id` properties to a type, to match the on-wire format of requests. -type WithJSONRPCRequest = T & { jsonrpc: '2.0'; id: SDKTypes.RequestId }; - -const sdkTypeChecks = { - RequestParams: (sdk: SDKTypes.RequestParams, spec: SpecTypes.RequestParams) => { - sdk = spec; - spec = sdk; - }, - NotificationParams: (sdk: SDKTypes.NotificationParams, spec: SpecTypes.NotificationParams) => { - sdk = spec; - spec = sdk; - }, - CancelledNotificationParams: (sdk: SDKTypes.CancelledNotificationParams, spec: SpecTypes.CancelledNotificationParams) => { - sdk = spec; - spec = sdk; - }, - InitializeRequestParams: (sdk: SDKTypes.InitializeRequestParams, spec: SpecTypes.InitializeRequestParams) => { - // @ts-expect-error 2025-11-25 types capabilities.experimental values as `object`; the SDK follows the 2026-07-28 schema's JSONObject - sdk = spec; - spec = sdk; - }, - ProgressNotificationParams: (sdk: SDKTypes.ProgressNotificationParams, spec: SpecTypes.ProgressNotificationParams) => { - sdk = spec; - spec = sdk; - }, - ResourceRequestParams: (sdk: SDKTypes.ResourceRequestParams, spec: SpecTypes.ResourceRequestParams) => { - sdk = spec; - spec = sdk; - }, - ReadResourceRequestParams: (sdk: SDKTypes.ReadResourceRequestParams, spec: SpecTypes.ReadResourceRequestParams) => { - sdk = spec; - spec = sdk; - }, - SubscribeRequestParams: (sdk: SDKTypes.SubscribeRequestParams, spec: SpecTypes.SubscribeRequestParams) => { - sdk = spec; - spec = sdk; - }, - UnsubscribeRequestParams: (sdk: SDKTypes.UnsubscribeRequestParams, spec: SpecTypes.UnsubscribeRequestParams) => { - sdk = spec; - spec = sdk; - }, - ResourceUpdatedNotificationParams: ( - sdk: SDKTypes.ResourceUpdatedNotificationParams, - spec: SpecTypes.ResourceUpdatedNotificationParams - ) => { - sdk = spec; - spec = sdk; - }, - GetPromptRequestParams: (sdk: SDKTypes.GetPromptRequestParams, spec: SpecTypes.GetPromptRequestParams) => { - sdk = spec; - spec = sdk; - }, - CallToolRequestParams: (sdk: SDKTypes.CallToolRequestParams, spec: SpecTypes.CallToolRequestParams) => { - sdk = spec; - spec = sdk; - }, - SetLevelRequestParams: (sdk: SDKTypes.SetLevelRequestParams, spec: SpecTypes.SetLevelRequestParams) => { - sdk = spec; - spec = sdk; - }, - LoggingMessageNotificationParams: ( - sdk: SDKTypes.LoggingMessageNotificationParams, - spec: SpecTypes.LoggingMessageNotificationParams - ) => { - sdk = spec; - spec = sdk; - }, - CreateMessageRequestParams: (sdk: SDKTypes.CreateMessageRequestParams, spec: SpecTypes.CreateMessageRequestParams) => { - // @ts-expect-error 2025-11-25 types `metadata` as `object`; the SDK follows the 2026-07-28 schema's JSONObject - sdk = spec; - // @ts-expect-error the SDK's JSONValue-typed tool inputSchema properties are not assignable to 2025-11-25's `object` - spec = sdk; - }, - CompleteRequestParams: (sdk: SDKTypes.CompleteRequestParams, spec: SpecTypes.CompleteRequestParams) => { - sdk = spec; - spec = sdk; - }, - ElicitRequestParams: (sdk: SDKTypes.ElicitRequestParams, spec: SpecTypes.ElicitRequestParams) => { - sdk = spec; - spec = sdk; - }, - ElicitRequestFormParams: (sdk: SDKTypes.ElicitRequestFormParams, spec: SpecTypes.ElicitRequestFormParams) => { - sdk = spec; - spec = sdk; - }, - ElicitRequestURLParams: (sdk: SDKTypes.ElicitRequestURLParams, spec: SpecTypes.ElicitRequestURLParams) => { - sdk = spec; - spec = sdk; - }, - ElicitationCompleteNotification: ( - sdk: WithJSONRPC, - spec: SpecTypes.ElicitationCompleteNotification - ) => { - sdk = spec; - spec = sdk; - }, - PaginatedRequestParams: (sdk: SDKTypes.PaginatedRequestParams, spec: SpecTypes.PaginatedRequestParams) => { - sdk = spec; - spec = sdk; - }, - CancelledNotification: (sdk: WithJSONRPC, spec: SpecTypes.CancelledNotification) => { - sdk = spec; - spec = sdk; - }, - BaseMetadata: (sdk: SDKTypes.BaseMetadata, spec: SpecTypes.BaseMetadata) => { - sdk = spec; - spec = sdk; - }, - Implementation: (sdk: SDKTypes.Implementation, spec: SpecTypes.Implementation) => { - sdk = spec; - spec = sdk; - }, - ProgressNotification: (sdk: WithJSONRPC, spec: SpecTypes.ProgressNotification) => { - sdk = spec; - spec = sdk; - }, - SubscribeRequest: (sdk: WithJSONRPCRequest, spec: SpecTypes.SubscribeRequest) => { - sdk = spec; - spec = sdk; - }, - UnsubscribeRequest: (sdk: WithJSONRPCRequest, spec: SpecTypes.UnsubscribeRequest) => { - sdk = spec; - spec = sdk; - }, - PaginatedRequest: (sdk: WithJSONRPCRequest, spec: SpecTypes.PaginatedRequest) => { - sdk = spec; - spec = sdk; - }, - PaginatedResult: (sdk: SDKTypes.PaginatedResult, spec: SpecTypes.PaginatedResult) => { - sdk = spec; - spec = sdk; - }, - ListRootsRequest: (sdk: WithJSONRPCRequest, spec: SpecTypes.ListRootsRequest) => { - sdk = spec; - spec = sdk; - }, - ListRootsResult: (sdk: SDKTypes.ListRootsResult, spec: SpecTypes.ListRootsResult) => { - sdk = spec; - spec = sdk; - }, - Root: (sdk: SDKTypes.Root, spec: SpecTypes.Root) => { - sdk = spec; - spec = sdk; - }, - ElicitRequest: (sdk: WithJSONRPCRequest, spec: SpecTypes.ElicitRequest) => { - sdk = spec; - spec = sdk; - }, - ElicitResult: (sdk: SDKTypes.ElicitResult, spec: SpecTypes.ElicitResult) => { - sdk = spec; - spec = sdk; - }, - CompleteRequest: (sdk: WithJSONRPCRequest, spec: SpecTypes.CompleteRequest) => { - sdk = spec; - spec = sdk; - }, - CompleteResult: (sdk: SDKTypes.CompleteResult, spec: SpecTypes.CompleteResult) => { - sdk = spec; - spec = sdk; - }, - ProgressToken: (sdk: SDKTypes.ProgressToken, spec: SpecTypes.ProgressToken) => { - sdk = spec; - spec = sdk; - }, - Cursor: (sdk: SDKTypes.Cursor, spec: SpecTypes.Cursor) => { - sdk = spec; - spec = sdk; - }, - Request: (sdk: SDKTypes.Request, spec: SpecTypes.Request) => { - sdk = spec; - spec = sdk; - }, - Result: (sdk: SDKTypes.Result, spec: SpecTypes.Result) => { - sdk = spec; - spec = sdk; - }, - RequestId: (sdk: SDKTypes.RequestId, spec: SpecTypes.RequestId) => { - sdk = spec; - spec = sdk; - }, - JSONRPCRequest: (sdk: SDKTypes.JSONRPCRequest, spec: SpecTypes.JSONRPCRequest) => { - sdk = spec; - spec = sdk; - }, - JSONRPCNotification: (sdk: SDKTypes.JSONRPCNotification, spec: SpecTypes.JSONRPCNotification) => { - sdk = spec; - spec = sdk; - }, - JSONRPCResponse: (sdk: SDKTypes.JSONRPCResponse, spec: SpecTypes.JSONRPCResponse) => { - sdk = spec; - spec = sdk; - }, - EmptyResult: (sdk: SDKTypes.EmptyResult, spec: SpecTypes.EmptyResult) => { - sdk = spec; - spec = sdk; - }, - Notification: (sdk: SDKTypes.Notification, spec: SpecTypes.Notification) => { - sdk = spec; - spec = sdk; - }, - ClientResult: (sdk: SDKTypes.ClientResult, spec: SpecTypes.ClientResult) => { - sdk = spec; - spec = sdk; - }, - ClientNotification: (sdk: WithJSONRPC, spec: SpecTypes.ClientNotification) => { - sdk = spec; - spec = sdk; - }, - ServerResult: (sdk: SDKTypes.ServerResult, spec: SpecTypes.ServerResult) => { - sdk = spec; - spec = sdk; - }, - ResourceTemplateReference: (sdk: SDKTypes.ResourceTemplateReference, spec: SpecTypes.ResourceTemplateReference) => { - sdk = spec; - spec = sdk; - }, - PromptReference: (sdk: SDKTypes.PromptReference, spec: SpecTypes.PromptReference) => { - sdk = spec; - spec = sdk; - }, - ToolAnnotations: (sdk: SDKTypes.ToolAnnotations, spec: SpecTypes.ToolAnnotations) => { - sdk = spec; - spec = sdk; - }, - Tool: (sdk: SDKTypes.Tool, spec: SpecTypes.Tool) => { - // @ts-expect-error 2025-11-25 types inputSchema/outputSchema properties as `object`; the SDK follows the 2026-07-28 schema's JSONValue - sdk = spec; - // @ts-expect-error the SDK's JSONValue-typed inputSchema/outputSchema properties are not assignable to 2025-11-25's `object` - spec = sdk; - }, - ListToolsRequest: (sdk: WithJSONRPCRequest, spec: SpecTypes.ListToolsRequest) => { - sdk = spec; - spec = sdk; - }, - ListToolsResult: (sdk: SDKTypes.ListToolsResult, spec: SpecTypes.ListToolsResult) => { - // @ts-expect-error 2025-11-25 vs 2026-07-28 Tool typing; see the Tool check above - sdk = spec; - // @ts-expect-error 2025-11-25 vs 2026-07-28 Tool typing; see the Tool check above - spec = sdk; - }, - CallToolResult: (sdk: SDKTypes.CallToolResult, spec: SpecTypes.CallToolResult) => { - sdk = spec; - spec = sdk; - }, - CallToolRequest: (sdk: WithJSONRPCRequest, spec: SpecTypes.CallToolRequest) => { - sdk = spec; - spec = sdk; - }, - ToolListChangedNotification: (sdk: WithJSONRPC, spec: SpecTypes.ToolListChangedNotification) => { - sdk = spec; - spec = sdk; - }, - ResourceListChangedNotification: ( - sdk: WithJSONRPC, - spec: SpecTypes.ResourceListChangedNotification - ) => { - sdk = spec; - spec = sdk; - }, - PromptListChangedNotification: ( - sdk: WithJSONRPC, - spec: SpecTypes.PromptListChangedNotification - ) => { - sdk = spec; - spec = sdk; - }, - RootsListChangedNotification: ( - sdk: WithJSONRPC, - spec: SpecTypes.RootsListChangedNotification - ) => { - sdk = spec; - spec = sdk; - }, - ResourceUpdatedNotification: (sdk: WithJSONRPC, spec: SpecTypes.ResourceUpdatedNotification) => { - sdk = spec; - spec = sdk; - }, - SamplingMessage: (sdk: SDKTypes.SamplingMessage, spec: SpecTypes.SamplingMessage) => { - sdk = spec; - spec = sdk; - }, - CreateMessageResult: (sdk: SDKTypes.CreateMessageResultWithTools, spec: SpecTypes.CreateMessageResult) => { - sdk = spec; - spec = sdk; - }, - SetLevelRequest: (sdk: WithJSONRPCRequest, spec: SpecTypes.SetLevelRequest) => { - sdk = spec; - spec = sdk; - }, - PingRequest: (sdk: WithJSONRPCRequest, spec: SpecTypes.PingRequest) => { - sdk = spec; - spec = sdk; - }, - InitializedNotification: (sdk: WithJSONRPC, spec: SpecTypes.InitializedNotification) => { - sdk = spec; - spec = sdk; - }, - ListResourcesRequest: (sdk: WithJSONRPCRequest, spec: SpecTypes.ListResourcesRequest) => { - sdk = spec; - spec = sdk; - }, - ListResourcesResult: (sdk: SDKTypes.ListResourcesResult, spec: SpecTypes.ListResourcesResult) => { - sdk = spec; - spec = sdk; - }, - ListResourceTemplatesRequest: ( - sdk: WithJSONRPCRequest, - spec: SpecTypes.ListResourceTemplatesRequest - ) => { - sdk = spec; - spec = sdk; - }, - ListResourceTemplatesResult: (sdk: SDKTypes.ListResourceTemplatesResult, spec: SpecTypes.ListResourceTemplatesResult) => { - sdk = spec; - spec = sdk; - }, - ReadResourceRequest: (sdk: WithJSONRPCRequest, spec: SpecTypes.ReadResourceRequest) => { - sdk = spec; - spec = sdk; - }, - ReadResourceResult: (sdk: SDKTypes.ReadResourceResult, spec: SpecTypes.ReadResourceResult) => { - sdk = spec; - spec = sdk; - }, - ResourceContents: (sdk: SDKTypes.ResourceContents, spec: SpecTypes.ResourceContents) => { - sdk = spec; - spec = sdk; - }, - TextResourceContents: (sdk: SDKTypes.TextResourceContents, spec: SpecTypes.TextResourceContents) => { - sdk = spec; - spec = sdk; - }, - BlobResourceContents: (sdk: SDKTypes.BlobResourceContents, spec: SpecTypes.BlobResourceContents) => { - sdk = spec; - spec = sdk; - }, - Resource: (sdk: SDKTypes.Resource, spec: SpecTypes.Resource) => { - sdk = spec; - spec = sdk; - }, - ResourceTemplate: (sdk: SDKTypes.ResourceTemplateType, spec: SpecTypes.ResourceTemplate) => { - sdk = spec; - spec = sdk; - }, - PromptArgument: (sdk: SDKTypes.PromptArgument, spec: SpecTypes.PromptArgument) => { - sdk = spec; - spec = sdk; - }, - Prompt: (sdk: SDKTypes.Prompt, spec: SpecTypes.Prompt) => { - sdk = spec; - spec = sdk; - }, - ListPromptsRequest: (sdk: WithJSONRPCRequest, spec: SpecTypes.ListPromptsRequest) => { - sdk = spec; - spec = sdk; - }, - ListPromptsResult: (sdk: SDKTypes.ListPromptsResult, spec: SpecTypes.ListPromptsResult) => { - sdk = spec; - spec = sdk; - }, - GetPromptRequest: (sdk: WithJSONRPCRequest, spec: SpecTypes.GetPromptRequest) => { - sdk = spec; - spec = sdk; - }, - TextContent: (sdk: SDKTypes.TextContent, spec: SpecTypes.TextContent) => { - sdk = spec; - spec = sdk; - }, - ImageContent: (sdk: SDKTypes.ImageContent, spec: SpecTypes.ImageContent) => { - sdk = spec; - spec = sdk; - }, - AudioContent: (sdk: SDKTypes.AudioContent, spec: SpecTypes.AudioContent) => { - sdk = spec; - spec = sdk; - }, - EmbeddedResource: (sdk: SDKTypes.EmbeddedResource, spec: SpecTypes.EmbeddedResource) => { - sdk = spec; - spec = sdk; - }, - ResourceLink: (sdk: SDKTypes.ResourceLink, spec: SpecTypes.ResourceLink) => { - sdk = spec; - spec = sdk; - }, - ContentBlock: (sdk: SDKTypes.ContentBlock, spec: SpecTypes.ContentBlock) => { - sdk = spec; - spec = sdk; - }, - PromptMessage: (sdk: SDKTypes.PromptMessage, spec: SpecTypes.PromptMessage) => { - sdk = spec; - spec = sdk; - }, - GetPromptResult: (sdk: SDKTypes.GetPromptResult, spec: SpecTypes.GetPromptResult) => { - sdk = spec; - spec = sdk; - }, - BooleanSchema: (sdk: SDKTypes.BooleanSchema, spec: SpecTypes.BooleanSchema) => { - sdk = spec; - spec = sdk; - }, - StringSchema: (sdk: SDKTypes.StringSchema, spec: SpecTypes.StringSchema) => { - sdk = spec; - spec = sdk; - }, - NumberSchema: (sdk: SDKTypes.NumberSchema, spec: SpecTypes.NumberSchema) => { - sdk = spec; - spec = sdk; - }, - EnumSchema: (sdk: SDKTypes.EnumSchema, spec: SpecTypes.EnumSchema) => { - sdk = spec; - spec = sdk; - }, - UntitledSingleSelectEnumSchema: (sdk: SDKTypes.UntitledSingleSelectEnumSchema, spec: SpecTypes.UntitledSingleSelectEnumSchema) => { - sdk = spec; - spec = sdk; - }, - TitledSingleSelectEnumSchema: (sdk: SDKTypes.TitledSingleSelectEnumSchema, spec: SpecTypes.TitledSingleSelectEnumSchema) => { - sdk = spec; - spec = sdk; - }, - SingleSelectEnumSchema: (sdk: SDKTypes.SingleSelectEnumSchema, spec: SpecTypes.SingleSelectEnumSchema) => { - sdk = spec; - spec = sdk; - }, - UntitledMultiSelectEnumSchema: (sdk: SDKTypes.UntitledMultiSelectEnumSchema, spec: SpecTypes.UntitledMultiSelectEnumSchema) => { - sdk = spec; - spec = sdk; - }, - TitledMultiSelectEnumSchema: (sdk: SDKTypes.TitledMultiSelectEnumSchema, spec: SpecTypes.TitledMultiSelectEnumSchema) => { - sdk = spec; - spec = sdk; - }, - MultiSelectEnumSchema: (sdk: SDKTypes.MultiSelectEnumSchema, spec: SpecTypes.MultiSelectEnumSchema) => { - sdk = spec; - spec = sdk; - }, - LegacyTitledEnumSchema: (sdk: SDKTypes.LegacyTitledEnumSchema, spec: SpecTypes.LegacyTitledEnumSchema) => { - sdk = spec; - spec = sdk; - }, - PrimitiveSchemaDefinition: (sdk: SDKTypes.PrimitiveSchemaDefinition, spec: SpecTypes.PrimitiveSchemaDefinition) => { - sdk = spec; - spec = sdk; - }, - JSONRPCErrorResponse: (sdk: SDKTypes.JSONRPCErrorResponse, spec: SpecTypes.JSONRPCErrorResponse) => { - sdk = spec; - spec = sdk; - }, - JSONRPCResultResponse: (sdk: SDKTypes.JSONRPCResultResponse, spec: SpecTypes.JSONRPCResultResponse) => { - sdk = spec; - spec = sdk; - }, - JSONRPCMessage: (sdk: SDKTypes.JSONRPCMessage, spec: SpecTypes.JSONRPCMessage) => { - sdk = spec; - spec = sdk; - }, - CreateMessageRequest: (sdk: WithJSONRPCRequest, spec: SpecTypes.CreateMessageRequest) => { - // @ts-expect-error 2025-11-25 vs 2026-07-28 typing of params metadata/tools; see the CreateMessageRequestParams check above - sdk = spec; - // @ts-expect-error 2025-11-25 vs 2026-07-28 typing of params metadata/tools; see the CreateMessageRequestParams check above - spec = sdk; - }, - InitializeRequest: (sdk: WithJSONRPCRequest, spec: SpecTypes.InitializeRequest) => { - // @ts-expect-error 2025-11-25 types capabilities.experimental values as `object`; the SDK follows the 2026-07-28 schema's JSONObject - sdk = spec; - spec = sdk; - }, - InitializeResult: (sdk: SDKTypes.InitializeResult, spec: SpecTypes.InitializeResult) => { - // @ts-expect-error 2025-11-25 types capabilities.experimental values as `object`; the SDK follows the 2026-07-28 schema's JSONObject - sdk = spec; - spec = sdk; - }, - ClientCapabilities: (sdk: SDKTypes.ClientCapabilities, spec: SpecTypes.ClientCapabilities) => { - // @ts-expect-error 2025-11-25 types experimental/sampling/elicitation/tasks blobs as `object`; the SDK follows the 2026-07-28 schema's JSONObject - sdk = spec; - spec = sdk; - }, - ServerCapabilities: (sdk: SDKTypes.ServerCapabilities, spec: SpecTypes.ServerCapabilities) => { - // @ts-expect-error 2025-11-25 types experimental/logging/completions/tasks blobs as `object`; the SDK follows the 2026-07-28 schema's JSONObject - sdk = spec; - spec = sdk; - }, - ClientRequest: (sdk: WithJSONRPCRequest, spec: SpecTypes.ClientRequest) => { - // @ts-expect-error 2025-11-25 types capabilities.experimental values as `object` (via the InitializeRequest member); the SDK follows the 2026-07-28 schema's JSONObject - sdk = spec; - spec = sdk; - }, - ServerRequest: (sdk: WithJSONRPCRequest, spec: SpecTypes.ServerRequest) => { - // @ts-expect-error 2025-11-25 vs 2026-07-28 typing of CreateMessageRequest params; see the CreateMessageRequestParams check above - sdk = spec; - // @ts-expect-error 2025-11-25 vs 2026-07-28 typing of CreateMessageRequest params; see the CreateMessageRequestParams check above - spec = sdk; - }, - LoggingMessageNotification: (sdk: WithJSONRPC, spec: SpecTypes.LoggingMessageNotification) => { - sdk = spec; - spec = sdk; - }, - ServerNotification: (sdk: WithJSONRPC, spec: SpecTypes.ServerNotification) => { - sdk = spec; - spec = sdk; - }, - LoggingLevel: (sdk: SDKTypes.LoggingLevel, spec: SpecTypes.LoggingLevel) => { - sdk = spec; - spec = sdk; - }, - Icon: (sdk: SDKTypes.Icon, spec: SpecTypes.Icon) => { - sdk = spec; - spec = sdk; - }, - Icons: (sdk: SDKTypes.Icons, spec: SpecTypes.Icons) => { - sdk = spec; - spec = sdk; - }, - ModelHint: (sdk: SDKTypes.ModelHint, spec: SpecTypes.ModelHint) => { - sdk = spec; - spec = sdk; - }, - ModelPreferences: (sdk: SDKTypes.ModelPreferences, spec: SpecTypes.ModelPreferences) => { - sdk = spec; - spec = sdk; - }, - ToolChoice: (sdk: SDKTypes.ToolChoice, spec: SpecTypes.ToolChoice) => { - sdk = spec; - spec = sdk; - }, - ToolUseContent: (sdk: SDKTypes.ToolUseContent, spec: SpecTypes.ToolUseContent) => { - sdk = spec; - spec = sdk; - }, - ToolResultContent: (sdk: SDKTypes.ToolResultContent, spec: SpecTypes.ToolResultContent) => { - sdk = spec; - spec = sdk; - }, - SamplingMessageContentBlock: (sdk: SDKTypes.SamplingMessageContentBlock, spec: SpecTypes.SamplingMessageContentBlock) => { - sdk = spec; - spec = sdk; - }, - Annotations: (sdk: SDKTypes.Annotations, spec: SpecTypes.Annotations) => { - sdk = spec; - spec = sdk; - }, - Role: (sdk: SDKTypes.Role, spec: SpecTypes.Role) => { - sdk = spec; - spec = sdk; - }, - ToolExecution: (sdk: SDKTypes.ToolExecution, spec: SpecTypes.ToolExecution) => { - sdk = spec; - spec = sdk; - }, - TaskStatus: (sdk: SDKTypes.TaskStatus, spec: SpecTypes.TaskStatus) => { - sdk = spec; - spec = sdk; - }, - TaskMetadata: (sdk: SDKTypes.TaskMetadata, spec: SpecTypes.TaskMetadata) => { - sdk = spec; - spec = sdk; - }, - RelatedTaskMetadata: (sdk: SDKTypes.RelatedTaskMetadata, spec: SpecTypes.RelatedTaskMetadata) => { - sdk = spec; - spec = sdk; - }, - TaskAugmentedRequestParams: (sdk: SDKTypes.TaskAugmentedRequestParams, spec: SpecTypes.TaskAugmentedRequestParams) => { - sdk = spec; - spec = sdk; - }, - Task: (sdk: SDKTypes.Task, spec: SpecTypes.Task) => { - sdk = spec; - spec = sdk; - }, - CreateTaskResult: (sdk: SDKTypes.CreateTaskResult, spec: SpecTypes.CreateTaskResult) => { - sdk = spec; - spec = sdk; - }, - GetTaskRequest: (sdk: WithJSONRPCRequest, spec: SpecTypes.GetTaskRequest) => { - sdk = spec; - spec = sdk; - }, - GetTaskResult: (sdk: SDKTypes.GetTaskResult, spec: SpecTypes.GetTaskResult) => { - sdk = spec; - spec = sdk; - }, - GetTaskPayloadRequest: (sdk: WithJSONRPCRequest, spec: SpecTypes.GetTaskPayloadRequest) => { - sdk = spec; - spec = sdk; - }, - GetTaskPayloadResult: (sdk: SDKTypes.GetTaskPayloadResult, spec: SpecTypes.GetTaskPayloadResult) => { - sdk = spec; - spec = sdk; - }, - ListTasksRequest: (sdk: WithJSONRPCRequest, spec: SpecTypes.ListTasksRequest) => { - sdk = spec; - spec = sdk; - }, - ListTasksResult: (sdk: SDKTypes.ListTasksResult, spec: SpecTypes.ListTasksResult) => { - sdk = spec; - spec = sdk; - }, - CancelTaskRequest: (sdk: WithJSONRPCRequest, spec: SpecTypes.CancelTaskRequest) => { - sdk = spec; - spec = sdk; - }, - CancelTaskResult: (sdk: SDKTypes.CancelTaskResult, spec: SpecTypes.CancelTaskResult) => { - sdk = spec; - spec = sdk; - }, - TaskStatusNotificationParams: (sdk: SDKTypes.TaskStatusNotificationParams, spec: SpecTypes.TaskStatusNotificationParams) => { - sdk = spec; - spec = sdk; - }, - TaskStatusNotification: (sdk: WithJSONRPC, spec: SpecTypes.TaskStatusNotification) => { - sdk = spec; - spec = sdk; - } -}; - -// --------------------------------------------------------------------------- -// Key-level assertions: verify that each SDK type and its corresponding spec -// type expose exactly the same set of named property keys. This catches cases -// where a Zod schema marks a field as `.optional()` but the spec does not (or -// vice-versa), which the mutual-assignability checks above cannot detect -// because optional fields satisfy structural subtyping in both directions. -// --------------------------------------------------------------------------- - -/** Strip index signatures, keeping only explicitly-named keys. */ -type KnownKeys = keyof { - [K in keyof T as string extends K ? never : number extends K ? never : symbol extends K ? never : K]: T[K]; -}; - -/** - * Assert that A and B have exactly the same set of known (named) keys. - * Resolves to `true` on match; a descriptive error type on mismatch. - */ -type AssertExactKeys< - A, - B, - Extra extends PropertyKey = Exclude, KnownKeys>, - Missing extends PropertyKey = Exclude, KnownKeys> -> = [Extra, Missing] extends [never, never] ? true : { _brand: 'KeyMismatch'; extra: Extra; missing: Missing }; - -/** Constraint: T must resolve to `true`. */ -type Assert = T; - -/** - * Same as {@link AssertExactKeys}, but tolerates the SDK's `resultType` key on - * result shapes: the SDK follows the 2026-07-28 schema's optional `resultType` - * passthrough (absent means "complete"), which is not in released 2025-11-25. - * Every other key still has to match exactly. - */ -type AssertExactKeysWithResultType = AssertExactKeys; - -/* - * Excluded from key-level assertions (21 entries): - * - * Union types — KnownKeys cannot meaningfully enumerate their members (15): - * ClientRequest, ServerRequest, ClientNotification, ServerNotification, - * ClientResult, ServerResult, JSONRPCMessage, JSONRPCResponse, ContentBlock, - * SamplingMessageContentBlock, ElicitRequestParams, PrimitiveSchemaDefinition, - * SingleSelectEnumSchema, MultiSelectEnumSchema, EnumSchema - * - * Primitive type aliases — no object keys to compare (6): - * Role, LoggingLevel, ProgressToken, RequestId, Cursor, TaskStatus - */ - -// -- Simple types (88) -- - -type _K_RequestParams = Assert>; -type _K_NotificationParams = Assert>; -type _K_CancelledNotificationParams = Assert>; -type _K_InitializeRequestParams = Assert>; -type _K_ProgressNotificationParams = Assert>; -type _K_ResourceRequestParams = Assert>; -type _K_ReadResourceRequestParams = Assert>; -type _K_SubscribeRequestParams = Assert>; -type _K_UnsubscribeRequestParams = Assert>; -type _K_ResourceUpdatedNotificationParams = Assert< - AssertExactKeys ->; -type _K_GetPromptRequestParams = Assert>; -type _K_CallToolRequestParams = Assert>; -type _K_SetLevelRequestParams = Assert>; -type _K_LoggingMessageNotificationParams = Assert< - AssertExactKeys ->; -type _K_CreateMessageRequestParams = Assert>; -type _K_CompleteRequestParams = Assert>; -type _K_ElicitRequestFormParams = Assert>; -type _K_ElicitRequestURLParams = Assert>; -type _K_PaginatedRequestParams = Assert>; -type _K_BaseMetadata = Assert>; -type _K_Implementation = Assert>; -type _K_PaginatedResult = Assert>; -type _K_ListRootsResult = Assert>; -type _K_Root = Assert>; -type _K_ElicitResult = Assert>; -type _K_CompleteResult = Assert>; -type _K_Request = Assert>; -type _K_Result = Assert>; -type _K_JSONRPCRequest = Assert>; -type _K_JSONRPCNotification = Assert>; -type _K_EmptyResult = Assert>; -type _K_Notification = Assert>; -type _K_ResourceTemplateReference = Assert>; -// @ts-expect-error Genuine mismatch: SDK PromptReference is missing 'title' from spec -type _K_PromptReference = Assert>; -type _K_ToolAnnotations = Assert>; -type _K_Tool = Assert>; -type _K_ListToolsResult = Assert>; -type _K_CallToolResult = Assert>; -type _K_ListResourcesResult = Assert>; -type _K_ListResourceTemplatesResult = Assert< - AssertExactKeysWithResultType ->; -type _K_ReadResourceResult = Assert>; -type _K_ResourceContents = Assert>; -type _K_TextResourceContents = Assert>; -type _K_BlobResourceContents = Assert>; -type _K_Resource = Assert>; -// @ts-expect-error Genuine mismatch: SDK PromptArgument is missing 'title' from spec -type _K_PromptArgument = Assert>; -type _K_Prompt = Assert>; -type _K_ListPromptsResult = Assert>; -type _K_GetPromptResult = Assert>; -type _K_TextContent = Assert>; -type _K_ImageContent = Assert>; -type _K_AudioContent = Assert>; -type _K_EmbeddedResource = Assert>; -type _K_ResourceLink = Assert>; -type _K_PromptMessage = Assert>; -type _K_BooleanSchema = Assert>; -type _K_StringSchema = Assert>; -type _K_NumberSchema = Assert>; -type _K_UntitledSingleSelectEnumSchema = Assert< - AssertExactKeys ->; -type _K_TitledSingleSelectEnumSchema = Assert< - AssertExactKeys ->; -type _K_UntitledMultiSelectEnumSchema = Assert< - AssertExactKeys ->; -type _K_TitledMultiSelectEnumSchema = Assert>; -type _K_LegacyTitledEnumSchema = Assert>; -type _K_JSONRPCErrorResponse = Assert>; -type _K_JSONRPCResultResponse = Assert>; -type _K_InitializeResult = Assert>; -// @ts-expect-error SDK follows the 2026-07-28 schema's `extensions` capability key; not in released 2025-11-25 -type _K_ClientCapabilities = Assert>; -// @ts-expect-error SDK follows the 2026-07-28 schema's `extensions` capability key; not in released 2025-11-25 -type _K_ServerCapabilities = Assert>; -type _K_SamplingMessage = Assert>; -type _K_Icon = Assert>; -type _K_Icons = Assert>; -type _K_ModelHint = Assert>; -type _K_ModelPreferences = Assert>; -type _K_ToolChoice = Assert>; -type _K_ToolUseContent = Assert>; -type _K_ToolResultContent = Assert>; -type _K_Annotations = Assert>; -type _K_ToolExecution = Assert>; -type _K_TaskMetadata = Assert>; -type _K_RelatedTaskMetadata = Assert>; -type _K_TaskAugmentedRequestParams = Assert>; -type _K_Task = Assert>; -type _K_CreateTaskResult = Assert>; -type _K_GetTaskResult = Assert>; -type _K_GetTaskPayloadResult = Assert>; -type _K_ListTasksResult = Assert>; -type _K_CancelTaskResult = Assert>; -type _K_TaskStatusNotificationParams = Assert< - AssertExactKeys ->; - -// -- WithJSONRPC-wrapped notification types (11) -- -// SDK notification types do not include `jsonrpc` — the spec types do. We wrap -// with WithJSONRPC<> to add the missing field before comparing keys. - -type _K_ElicitationCompleteNotification = Assert< - AssertExactKeys, SpecTypes.ElicitationCompleteNotification> ->; -type _K_CancelledNotification = Assert, SpecTypes.CancelledNotification>>; -type _K_ProgressNotification = Assert, SpecTypes.ProgressNotification>>; -type _K_ToolListChangedNotification = Assert< - AssertExactKeys, SpecTypes.ToolListChangedNotification> ->; -type _K_ResourceListChangedNotification = Assert< - AssertExactKeys, SpecTypes.ResourceListChangedNotification> ->; -type _K_PromptListChangedNotification = Assert< - AssertExactKeys, SpecTypes.PromptListChangedNotification> ->; -type _K_RootsListChangedNotification = Assert< - AssertExactKeys, SpecTypes.RootsListChangedNotification> ->; -type _K_ResourceUpdatedNotification = Assert< - AssertExactKeys, SpecTypes.ResourceUpdatedNotification> ->; -type _K_LoggingMessageNotification = Assert< - AssertExactKeys, SpecTypes.LoggingMessageNotification> ->; -type _K_InitializedNotification = Assert, SpecTypes.InitializedNotification>>; -type _K_TaskStatusNotification = Assert, SpecTypes.TaskStatusNotification>>; - -// -- WithJSONRPCRequest-wrapped request types (21) -- -// SDK request types do not include `jsonrpc` or `id` — the spec types do. We -// wrap with WithJSONRPCRequest<> to add the missing fields before comparing keys. - -type _K_SubscribeRequest = Assert, SpecTypes.SubscribeRequest>>; -type _K_UnsubscribeRequest = Assert, SpecTypes.UnsubscribeRequest>>; -type _K_PaginatedRequest = Assert, SpecTypes.PaginatedRequest>>; -type _K_ListRootsRequest = Assert, SpecTypes.ListRootsRequest>>; -type _K_ElicitRequest = Assert, SpecTypes.ElicitRequest>>; -type _K_CompleteRequest = Assert, SpecTypes.CompleteRequest>>; -type _K_ListToolsRequest = Assert, SpecTypes.ListToolsRequest>>; -type _K_CallToolRequest = Assert, SpecTypes.CallToolRequest>>; -type _K_SetLevelRequest = Assert, SpecTypes.SetLevelRequest>>; -type _K_PingRequest = Assert, SpecTypes.PingRequest>>; -type _K_ListResourcesRequest = Assert, SpecTypes.ListResourcesRequest>>; -type _K_ListResourceTemplatesRequest = Assert< - AssertExactKeys, SpecTypes.ListResourceTemplatesRequest> ->; -type _K_ReadResourceRequest = Assert, SpecTypes.ReadResourceRequest>>; -type _K_ListPromptsRequest = Assert, SpecTypes.ListPromptsRequest>>; -type _K_GetPromptRequest = Assert, SpecTypes.GetPromptRequest>>; -type _K_CreateMessageRequest = Assert, SpecTypes.CreateMessageRequest>>; -type _K_InitializeRequest = Assert, SpecTypes.InitializeRequest>>; -type _K_GetTaskRequest = Assert, SpecTypes.GetTaskRequest>>; -type _K_GetTaskPayloadRequest = Assert< - AssertExactKeys, SpecTypes.GetTaskPayloadRequest> ->; -type _K_ListTasksRequest = Assert, SpecTypes.ListTasksRequest>>; -type _K_CancelTaskRequest = Assert, SpecTypes.CancelTaskRequest>>; - -// -- Name mismatches (2) -- -// SDK exports these under different names than the spec. - -type _K_CreateMessageResult = Assert>; -type _K_ResourceTemplate = Assert>; - -// Types excluded from the key-parity completeness guard: union types and primitive aliases -// that cannot have meaningful AssertExactKeys assertions. -const KEY_PARITY_EXCLUDED = [ - // Union types (15) - 'ClientRequest', - 'ServerRequest', - 'ClientNotification', - 'ServerNotification', - 'ClientResult', - 'ServerResult', - 'JSONRPCMessage', - 'JSONRPCResponse', - 'ContentBlock', - 'SamplingMessageContentBlock', - 'ElicitRequestParams', - 'PrimitiveSchemaDefinition', - 'SingleSelectEnumSchema', - 'MultiSelectEnumSchema', - 'EnumSchema', - // Primitive aliases (6) - 'Role', - 'LoggingLevel', - 'ProgressToken', - 'RequestId', - 'Cursor', - 'TaskStatus' -]; - -// Generated from the frozen 2025-11-25 release schema by `pnpm run fetch:spec-types 2025-11-25`. -const SPEC_TYPES_FILE = path.resolve(__dirname, '../src/types/spec.types.2025-11-25.ts'); -const SDK_TYPES_FILE = path.resolve(__dirname, '../src/types/types.ts'); - -const MISSING_SDK_TYPES = [ - // These are inlined in the SDK: - 'Error', // The inner error object of a JSONRPCError - 'URLElicitationRequiredError' // In the SDK, but with a custom definition -]; - -function extractExportedTypes(source: string): string[] { - const matches = [...source.matchAll(/export\s+(?:interface|class|type)\s+(\w+)\b/g)]; - return matches.map(m => m[1]!); -} - -function extractKeyParityTypes(source: string): string[] { - return [...source.matchAll(/^type _K_(\w+)\s*=/gm)].map(m => m[1]!); -} - -describe('Spec Types (2025-11-25)', () => { - const specTypes = extractExportedTypes(fs.readFileSync(SPEC_TYPES_FILE, 'utf8')); - const sdkTypes = extractExportedTypes(fs.readFileSync(SDK_TYPES_FILE, 'utf8')); - const typesToCheck = specTypes.filter(type => !MISSING_SDK_TYPES.includes(type)); - - it('should define some expected types', () => { - expect(specTypes).toContain('JSONRPCNotification'); - expect(specTypes).toContain('ElicitResult'); - expect(specTypes).toHaveLength(145); - }); - - it('should have up to date list of missing sdk types', () => { - for (const typeName of MISSING_SDK_TYPES) { - expect(sdkTypes).not.toContain(typeName); - } - }); - - it('should have comprehensive compatibility tests', () => { - const missingTests = []; - - for (const typeName of typesToCheck) { - if (!sdkTypeChecks[typeName as keyof typeof sdkTypeChecks]) { - missingTests.push(typeName); - } - } - - expect(missingTests).toHaveLength(0); - }); - - it('should have key-parity assertions for all non-excluded compatibility tests', () => { - const thisSource = fs.readFileSync(__filename, 'utf8'); - const checked = new Set(extractKeyParityTypes(thisSource)); - const excluded = new Set(KEY_PARITY_EXCLUDED); - const missing = Object.keys(sdkTypeChecks).filter(name => !checked.has(name) && !excluded.has(name)); - expect(missing).toHaveLength(0); - }); - - describe('Missing SDK Types', () => { - it.each(MISSING_SDK_TYPES)('%s should not be present in MISSING_SDK_TYPES if it has a compatibility test', type => { - expect(sdkTypeChecks[type as keyof typeof sdkTypeChecks]).toBeUndefined(); - }); - }); -}); diff --git a/packages/core/test/spec.types.2026-07-28.test.ts b/packages/core/test/spec.types.2026-07-28.test.ts deleted file mode 100644 index 7360f2dc79..0000000000 --- a/packages/core/test/spec.types.2026-07-28.test.ts +++ /dev/null @@ -1,550 +0,0 @@ -/** - * Compares the SDK's types against the upcoming 2026-07-28 schema (spec.types.2026-07-28.ts). - * The frozen-release comparison lives in spec.types.2025-11-25.test.ts. - * - * The SDK does not implement the 2026-07-28 surface yet: every 2026-07-28 type whose shape the SDK - * does not (yet) match is listed in MISSING_SDK_TYPES_2026_07_28 below. Removing a name from - * that list forces a real mutual-assignability check to be added to sdkTypeChecks (the - * completeness tests below fail otherwise) — implementation work burns the list down. - * - * Unlike MISSING_SDK_TYPES in the 2025-11-25 comparison, names in this list may well - * exist in the SDK (e.g. RequestParams) — they are listed because the 2026-07-28 revision changed - * their shape, not necessarily because the SDK lacks them. - */ -import fs from 'node:fs'; -import path from 'node:path'; - -import { - LATEST_PROTOCOL_VERSION, - MISSING_REQUIRED_CLIENT_CAPABILITY, - UNSUPPORTED_PROTOCOL_VERSION -} from '../src/types/spec.types.2026-07-28'; -import type * as SpecTypes from '../src/types/spec.types.2026-07-28'; -import type * as SDKTypes from '../src/types/index'; -import { - CLIENT_CAPABILITIES_META_KEY, - CLIENT_INFO_META_KEY, - LOG_LEVEL_META_KEY, - PROTOCOL_VERSION_META_KEY, - ProtocolErrorCode -} from '../src/types/index'; - -/* eslint-disable @typescript-eslint/no-unused-vars */ - -// Adds the `jsonrpc` property to a type, to match the on-wire format of notifications. -type WithJSONRPC = T & { jsonrpc: '2.0' }; - -// Adds the `jsonrpc` and `id` properties to a type, to match the on-wire format of requests. -type WithJSONRPCRequest = T & { jsonrpc: '2.0'; id: SDKTypes.RequestId }; - -const sdkTypeChecks = { - JSONValue: (sdk: SDKTypes.JSONValue, spec: SpecTypes.JSONValue) => { - sdk = spec; - spec = sdk; - }, - JSONObject: (sdk: SDKTypes.JSONObject, spec: SpecTypes.JSONObject) => { - sdk = spec; - spec = sdk; - }, - JSONArray: (sdk: SDKTypes.JSONArray, spec: SpecTypes.JSONArray) => { - sdk = spec; - spec = sdk; - }, - MetaObject: (sdk: SDKTypes.MetaObject, spec: SpecTypes.MetaObject) => { - sdk = spec; - spec = sdk; - }, - // The SDK models the 2026-07-28 revision's required per-request `_meta` envelope as - // RequestMetaEnvelope (the base request schemas stay lenient; envelope - // requiredness is enforced at dispatch). This check also pins the - // *_META_KEY constants: a drifted key name breaks mutual assignability. - RequestMetaObject: (sdk: SDKTypes.RequestMetaEnvelope, spec: SpecTypes.RequestMetaObject) => { - sdk = spec; - spec = sdk; - }, - ProgressToken: (sdk: SDKTypes.ProgressToken, spec: SpecTypes.ProgressToken) => { - sdk = spec; - spec = sdk; - }, - Cursor: (sdk: SDKTypes.Cursor, spec: SpecTypes.Cursor) => { - sdk = spec; - spec = sdk; - }, - Request: (sdk: SDKTypes.Request, spec: SpecTypes.Request) => { - sdk = spec; - spec = sdk; - }, - NotificationParams: (sdk: SDKTypes.NotificationParams, spec: SpecTypes.NotificationParams) => { - sdk = spec; - spec = sdk; - }, - RequestId: (sdk: SDKTypes.RequestId, spec: SpecTypes.RequestId) => { - sdk = spec; - spec = sdk; - }, - JSONRPCRequest: (sdk: SDKTypes.JSONRPCRequest, spec: SpecTypes.JSONRPCRequest) => { - sdk = spec; - spec = sdk; - }, - JSONRPCNotification: (sdk: WithJSONRPC, spec: SpecTypes.JSONRPCNotification) => { - sdk = spec; - spec = sdk; - }, - JSONRPCErrorResponse: (sdk: SDKTypes.JSONRPCErrorResponse, spec: SpecTypes.JSONRPCErrorResponse) => { - sdk = spec; - spec = sdk; - }, - ParseError: (sdk: SDKTypes.ParseError, spec: SpecTypes.ParseError) => { - sdk = spec; - spec = sdk; - }, - InvalidRequestError: (sdk: SDKTypes.InvalidRequestError, spec: SpecTypes.InvalidRequestError) => { - sdk = spec; - spec = sdk; - }, - MethodNotFoundError: (sdk: SDKTypes.MethodNotFoundError, spec: SpecTypes.MethodNotFoundError) => { - sdk = spec; - spec = sdk; - }, - InvalidParamsError: (sdk: SDKTypes.InvalidParamsError, spec: SpecTypes.InvalidParamsError) => { - sdk = spec; - spec = sdk; - }, - InternalError: (sdk: SDKTypes.InternalError, spec: SpecTypes.InternalError) => { - sdk = spec; - spec = sdk; - }, - CancelledNotificationParams: (sdk: SDKTypes.CancelledNotificationParams, spec: SpecTypes.CancelledNotificationParams) => { - sdk = spec; - spec = sdk; - }, - CancelledNotification: (sdk: WithJSONRPC, spec: SpecTypes.CancelledNotification) => { - sdk = spec; - spec = sdk; - }, - ClientCapabilities: (sdk: SDKTypes.ClientCapabilities, spec: SpecTypes.ClientCapabilities) => { - sdk = spec; - spec = sdk; - }, - ServerCapabilities: (sdk: SDKTypes.ServerCapabilities, spec: SpecTypes.ServerCapabilities) => { - sdk = spec; - spec = sdk; - }, - Icon: (sdk: SDKTypes.Icon, spec: SpecTypes.Icon) => { - sdk = spec; - spec = sdk; - }, - Icons: (sdk: SDKTypes.Icons, spec: SpecTypes.Icons) => { - sdk = spec; - spec = sdk; - }, - BaseMetadata: (sdk: SDKTypes.BaseMetadata, spec: SpecTypes.BaseMetadata) => { - sdk = spec; - spec = sdk; - }, - Implementation: (sdk: SDKTypes.Implementation, spec: SpecTypes.Implementation) => { - sdk = spec; - spec = sdk; - }, - ProgressNotificationParams: (sdk: SDKTypes.ProgressNotificationParams, spec: SpecTypes.ProgressNotificationParams) => { - sdk = spec; - spec = sdk; - }, - ProgressNotification: (sdk: WithJSONRPC, spec: SpecTypes.ProgressNotification) => { - sdk = spec; - spec = sdk; - }, - ResourceListChangedNotification: ( - sdk: WithJSONRPC, - spec: SpecTypes.ResourceListChangedNotification - ) => { - sdk = spec; - spec = sdk; - }, - ResourceUpdatedNotificationParams: ( - sdk: SDKTypes.ResourceUpdatedNotificationParams, - spec: SpecTypes.ResourceUpdatedNotificationParams - ) => { - sdk = spec; - spec = sdk; - }, - ResourceUpdatedNotification: (sdk: WithJSONRPC, spec: SpecTypes.ResourceUpdatedNotification) => { - sdk = spec; - spec = sdk; - }, - Resource: (sdk: SDKTypes.Resource, spec: SpecTypes.Resource) => { - sdk = spec; - spec = sdk; - }, - ResourceTemplate: (sdk: SDKTypes.ResourceTemplateType, spec: SpecTypes.ResourceTemplate) => { - sdk = spec; - spec = sdk; - }, - ResourceContents: (sdk: SDKTypes.ResourceContents, spec: SpecTypes.ResourceContents) => { - sdk = spec; - spec = sdk; - }, - TextResourceContents: (sdk: SDKTypes.TextResourceContents, spec: SpecTypes.TextResourceContents) => { - sdk = spec; - spec = sdk; - }, - BlobResourceContents: (sdk: SDKTypes.BlobResourceContents, spec: SpecTypes.BlobResourceContents) => { - sdk = spec; - spec = sdk; - }, - Prompt: (sdk: SDKTypes.Prompt, spec: SpecTypes.Prompt) => { - sdk = spec; - spec = sdk; - }, - PromptArgument: (sdk: SDKTypes.PromptArgument, spec: SpecTypes.PromptArgument) => { - sdk = spec; - spec = sdk; - }, - Role: (sdk: SDKTypes.Role, spec: SpecTypes.Role) => { - sdk = spec; - spec = sdk; - }, - PromptMessage: (sdk: SDKTypes.PromptMessage, spec: SpecTypes.PromptMessage) => { - sdk = spec; - spec = sdk; - }, - ResourceLink: (sdk: SDKTypes.ResourceLink, spec: SpecTypes.ResourceLink) => { - sdk = spec; - spec = sdk; - }, - EmbeddedResource: (sdk: SDKTypes.EmbeddedResource, spec: SpecTypes.EmbeddedResource) => { - sdk = spec; - spec = sdk; - }, - PromptListChangedNotification: ( - sdk: WithJSONRPC, - spec: SpecTypes.PromptListChangedNotification - ) => { - sdk = spec; - spec = sdk; - }, - ToolListChangedNotification: (sdk: WithJSONRPC, spec: SpecTypes.ToolListChangedNotification) => { - sdk = spec; - spec = sdk; - }, - ToolAnnotations: (sdk: SDKTypes.ToolAnnotations, spec: SpecTypes.ToolAnnotations) => { - sdk = spec; - spec = sdk; - }, - LoggingMessageNotificationParams: ( - sdk: SDKTypes.LoggingMessageNotificationParams, - spec: SpecTypes.LoggingMessageNotificationParams - ) => { - sdk = spec; - spec = sdk; - }, - LoggingMessageNotification: (sdk: WithJSONRPC, spec: SpecTypes.LoggingMessageNotification) => { - sdk = spec; - spec = sdk; - }, - LoggingLevel: (sdk: SDKTypes.LoggingLevel, spec: SpecTypes.LoggingLevel) => { - sdk = spec; - spec = sdk; - }, - ToolChoice: (sdk: SDKTypes.ToolChoice, spec: SpecTypes.ToolChoice) => { - sdk = spec; - spec = sdk; - }, - Annotations: (sdk: SDKTypes.Annotations, spec: SpecTypes.Annotations) => { - sdk = spec; - spec = sdk; - }, - ContentBlock: (sdk: SDKTypes.ContentBlock, spec: SpecTypes.ContentBlock) => { - sdk = spec; - spec = sdk; - }, - TextContent: (sdk: SDKTypes.TextContent, spec: SpecTypes.TextContent) => { - sdk = spec; - spec = sdk; - }, - ImageContent: (sdk: SDKTypes.ImageContent, spec: SpecTypes.ImageContent) => { - sdk = spec; - spec = sdk; - }, - AudioContent: (sdk: SDKTypes.AudioContent, spec: SpecTypes.AudioContent) => { - sdk = spec; - spec = sdk; - }, - ToolUseContent: (sdk: SDKTypes.ToolUseContent, spec: SpecTypes.ToolUseContent) => { - sdk = spec; - spec = sdk; - }, - ModelPreferences: (sdk: SDKTypes.ModelPreferences, spec: SpecTypes.ModelPreferences) => { - sdk = spec; - spec = sdk; - }, - ModelHint: (sdk: SDKTypes.ModelHint, spec: SpecTypes.ModelHint) => { - sdk = spec; - spec = sdk; - }, - ResourceTemplateReference: (sdk: SDKTypes.ResourceTemplateReference, spec: SpecTypes.ResourceTemplateReference) => { - sdk = spec; - spec = sdk; - }, - PromptReference: (sdk: SDKTypes.PromptReference, spec: SpecTypes.PromptReference) => { - sdk = spec; - spec = sdk; - }, - Root: (sdk: SDKTypes.Root, spec: SpecTypes.Root) => { - sdk = spec; - spec = sdk; - }, - ElicitRequestFormParams: (sdk: SDKTypes.ElicitRequestFormParams, spec: SpecTypes.ElicitRequestFormParams) => { - sdk = spec; - spec = sdk; - }, - ElicitRequestURLParams: (sdk: SDKTypes.ElicitRequestURLParams, spec: SpecTypes.ElicitRequestURLParams) => { - sdk = spec; - spec = sdk; - }, - ElicitRequestParams: (sdk: SDKTypes.ElicitRequestParams, spec: SpecTypes.ElicitRequestParams) => { - sdk = spec; - spec = sdk; - }, - ElicitRequest: (sdk: SDKTypes.ElicitRequest, spec: SpecTypes.ElicitRequest) => { - sdk = spec; - spec = sdk; - }, - PrimitiveSchemaDefinition: (sdk: SDKTypes.PrimitiveSchemaDefinition, spec: SpecTypes.PrimitiveSchemaDefinition) => { - sdk = spec; - spec = sdk; - }, - StringSchema: (sdk: SDKTypes.StringSchema, spec: SpecTypes.StringSchema) => { - sdk = spec; - spec = sdk; - }, - NumberSchema: (sdk: SDKTypes.NumberSchema, spec: SpecTypes.NumberSchema) => { - sdk = spec; - spec = sdk; - }, - BooleanSchema: (sdk: SDKTypes.BooleanSchema, spec: SpecTypes.BooleanSchema) => { - sdk = spec; - spec = sdk; - }, - UntitledSingleSelectEnumSchema: (sdk: SDKTypes.UntitledSingleSelectEnumSchema, spec: SpecTypes.UntitledSingleSelectEnumSchema) => { - sdk = spec; - spec = sdk; - }, - TitledSingleSelectEnumSchema: (sdk: SDKTypes.TitledSingleSelectEnumSchema, spec: SpecTypes.TitledSingleSelectEnumSchema) => { - sdk = spec; - spec = sdk; - }, - SingleSelectEnumSchema: (sdk: SDKTypes.SingleSelectEnumSchema, spec: SpecTypes.SingleSelectEnumSchema) => { - sdk = spec; - spec = sdk; - }, - UntitledMultiSelectEnumSchema: (sdk: SDKTypes.UntitledMultiSelectEnumSchema, spec: SpecTypes.UntitledMultiSelectEnumSchema) => { - sdk = spec; - spec = sdk; - }, - TitledMultiSelectEnumSchema: (sdk: SDKTypes.TitledMultiSelectEnumSchema, spec: SpecTypes.TitledMultiSelectEnumSchema) => { - sdk = spec; - spec = sdk; - }, - MultiSelectEnumSchema: (sdk: SDKTypes.MultiSelectEnumSchema, spec: SpecTypes.MultiSelectEnumSchema) => { - sdk = spec; - spec = sdk; - }, - LegacyTitledEnumSchema: (sdk: SDKTypes.LegacyTitledEnumSchema, spec: SpecTypes.LegacyTitledEnumSchema) => { - sdk = spec; - spec = sdk; - }, - EnumSchema: (sdk: SDKTypes.EnumSchema, spec: SpecTypes.EnumSchema) => { - sdk = spec; - spec = sdk; - }, - ElicitationCompleteNotification: ( - sdk: WithJSONRPC, - spec: SpecTypes.ElicitationCompleteNotification - ) => { - sdk = spec; - spec = sdk; - }, - ClientNotification: (sdk: WithJSONRPC, spec: SpecTypes.ClientNotification) => { - sdk = spec; - spec = sdk; - } -}; - -// Generated from the 2026-07-28 schema by `pnpm run fetch:spec-types 2026-07-28 `. -const SPEC_TYPES_FILE = path.resolve(__dirname, '../src/types/spec.types.2026-07-28.ts'); - -/** - * 2026-07-28 spec types the SDK does not match yet. Spec-implementation work for the - * 2026-07-28 release removes entries from this list as the SDK adopts each shape. - */ -const MISSING_SDK_TYPES_2026_07_28 = [ - // Inlined in the SDK (same as the 2025-11-25 comparison): - 'Error', // The inner error object of a JSONRPCError - - // SEP-2575 per-request envelope: 2026-07-28 requests REQUIRE a `_meta` envelope - // (`io.modelcontextprotocol/protocolVersion`, clientInfo, clientCapabilities). The - // envelope itself is modeled by RequestMetaEnvelope (see sdkTypeChecks above); the - // request shapes below stay here because the SDK wire schemas deliberately keep - // `_meta` lenient — the same schemas parse pre-2026 requests (no envelope) and 2026 - // requests, with envelope requiredness enforced per request at dispatch. They burn - // only if the SDK ever models era-specific request types. - 'RequestParams', - 'PaginatedRequestParams', - 'ResourceRequestParams', - 'CallToolRequestParams', - 'CompleteRequestParams', - 'GetPromptRequestParams', - 'ReadResourceRequestParams', - 'CreateMessageRequestParams', - 'PaginatedRequest', - 'CallToolRequest', - 'CompleteRequest', - 'GetPromptRequest', - 'ListPromptsRequest', - 'ListResourceTemplatesRequest', - 'ListResourcesRequest', - 'ListRootsRequest', - 'ListToolsRequest', - 'ReadResourceRequest', - 'CreateMessageRequest', - 'ClientRequest', - - // SEP-2322 (MRTR) → PR for MRTR: 2026-07-28 results carry a required `resultType` - // discriminator. The SDK base result schema carries `resultType` as an optional - // passthrough only (absent means "complete"); per-result modeling lands with MRTR. - 'Result', - 'EmptyResult', - 'PaginatedResult', - 'CallToolResult', - 'CompleteResult', - 'ElicitResult', - 'GetPromptResult', - 'ListPromptsResult', - 'ListResourceTemplatesResult', - 'ListResourcesResult', - 'ListRootsResult', - 'ListToolsResult', - 'ReadResourceResult', - 'CreateMessageResult', - 'ClientResult', - 'ServerResult', - 'ResultType', - - // SEP-2549 cacheable results: `ttlMs`/`cacheScope` caching hints on the list/read - // result shapes → PR for SEP-2549: - 'CacheableResult', - - // Response envelopes embedding the changed Result shape → PR for MRTR: - 'JSONRPCResultResponse', - 'JSONRPCResponse', - 'JSONRPCMessage', - 'CallToolResultResponse', - 'CompleteResultResponse', - 'GetPromptResultResponse', - 'ListPromptsResultResponse', - 'ListResourceTemplatesResultResponse', - 'ListResourcesResultResponse', - 'ListToolsResultResponse', - 'ReadResourceResultResponse', - - // SEP-2575 sessionless discovery: the SDK ships the wire shapes - // (DiscoverRequestSchema / DiscoverResultSchema), but the 2026-07-28 shapes embed the - // required `_meta` envelope (request) and required `resultType` (result → MRTR PR), - // so they do not match yet; DiscoverResultResponse is a response wrapper (→ MRTR PR): - 'DiscoverRequest', - 'DiscoverResult', - 'DiscoverResultResponse', - - // SEP-2567 input requests/responses (new surface) → PR for MRTR: - 'InputRequest', - 'InputRequests', - 'InputRequiredResult', - 'InputResponse', - 'InputResponseRequestParams', - 'InputResponses', - - // 2026-07-28 subscriptions surface (new) → PR for subscriptions/listen: - 'SubscriptionFilter', - 'SubscriptionsAcknowledgedNotification', - 'SubscriptionsAcknowledgedNotificationParams', - 'SubscriptionsListenRequest', - 'SubscriptionsListenRequestParams', - - // New typed protocol errors: the SDK ships -32003/-32004 as ProtocolErrorCode - // entries plus the UnsupportedProtocolVersionError class (errors.ts); the spec's - // per-code error *response envelope* interfaces are not modeled as wire types: - 'MissingRequiredClientCapabilityError', - 'UnsupportedProtocolVersionError', - - // Other shapes changed in the 2026-07-28 schema: sampling content changes (SamplingMessage, - // SamplingMessageContentBlock, ToolResultContent) → backchannel PR; open tool - // input/output schema typing (Tool); loosened Notification.params (Notification); - // server notification union, which gains the subscriptions ack (ServerNotification → - // PR for subscriptions/listen): - 'SamplingMessage', - 'SamplingMessageContentBlock', - 'ToolResultContent', - 'Tool', - 'Notification', - 'ServerNotification' -]; - -function extractExportedTypes(source: string): string[] { - const matches = [...source.matchAll(/export\s+(?:interface|class|type)\s+(\w+)\b/g)]; - return matches.map(m => m[1]!); -} - -describe('Spec Types (2026-07-28)', () => { - const specTypes = extractExportedTypes(fs.readFileSync(SPEC_TYPES_FILE, 'utf8')); - const typesToCheck = specTypes.filter(type => !MISSING_SDK_TYPES_2026_07_28.includes(type)); - - it('pins the 2026-07-28 protocol version and the new error codes', () => { - expect(LATEST_PROTOCOL_VERSION).toBe('2026-07-28'); - expect(MISSING_REQUIRED_CLIENT_CAPABILITY).toBe(-32003); - expect(UNSUPPORTED_PROTOCOL_VERSION).toBe(-32004); - expect(ProtocolErrorCode.MissingRequiredClientCapability).toBe(MISSING_REQUIRED_CLIENT_CAPABILITY); - expect(ProtocolErrorCode.UnsupportedProtocolVersion).toBe(UNSUPPORTED_PROTOCOL_VERSION); - }); - - it('pins the per-request _meta envelope keys to the 2026-07-28 schema', () => { - expect(PROTOCOL_VERSION_META_KEY).toBe('io.modelcontextprotocol/protocolVersion'); - expect(CLIENT_INFO_META_KEY).toBe('io.modelcontextprotocol/clientInfo'); - expect(CLIENT_CAPABILITIES_META_KEY).toBe('io.modelcontextprotocol/clientCapabilities'); - expect(LOG_LEVEL_META_KEY).toBe('io.modelcontextprotocol/logLevel'); - }); - - it('should define some expected types', () => { - expect(specTypes).toContain('DiscoverRequest'); - expect(specTypes).toContain('InputRequiredResult'); - expect(specTypes).toContain('SubscriptionsListenRequest'); - expect(specTypes).toHaveLength(150); - }); - - it('should only allowlist types that exist in the 2026-07-28 schema', () => { - for (const typeName of MISSING_SDK_TYPES_2026_07_28) { - expect(specTypes).toContain(typeName); - } - }); - - it('should have comprehensive compatibility tests', () => { - const missingTests = []; - - for (const typeName of typesToCheck) { - if (!sdkTypeChecks[typeName as keyof typeof sdkTypeChecks]) { - missingTests.push(typeName); - } - } - - expect(missingTests).toHaveLength(0); - }); - - describe('Missing SDK Types', () => { - it.each(MISSING_SDK_TYPES_2026_07_28)( - '%s should not be present in MISSING_SDK_TYPES_2026_07_28 if it has a compatibility test', - type => { - expect(sdkTypeChecks[type as keyof typeof sdkTypeChecks]).toBeUndefined(); - } - ); - }); -}); diff --git a/packages/core/test/types.capabilities.test.ts b/packages/core/test/types.capabilities.test.ts deleted file mode 100644 index 18e7d9745c..0000000000 --- a/packages/core/test/types.capabilities.test.ts +++ /dev/null @@ -1,103 +0,0 @@ -import { ClientCapabilitiesSchema, InitializeRequestParamsSchema } from '../src/types/index'; - -describe('ClientCapabilitiesSchema backwards compatibility', () => { - describe('ElicitationCapabilitySchema preprocessing', () => { - it('should inject form capability when elicitation is an empty object', () => { - const capabilities = { - elicitation: {} - }; - - const result = ClientCapabilitiesSchema.parse(capabilities); - expect(result.elicitation).toBeDefined(); - expect(result.elicitation?.form).toBeDefined(); - expect(result.elicitation?.form).toEqual({}); - expect(result.elicitation?.url).toBeUndefined(); - }); - - it('should preserve form capability configuration including applyDefaults', () => { - const capabilities = { - elicitation: { - form: { - applyDefaults: true - } - } - }; - - const result = ClientCapabilitiesSchema.parse(capabilities); - expect(result.elicitation).toBeDefined(); - expect(result.elicitation?.form).toBeDefined(); - expect(result.elicitation?.form).toEqual({ applyDefaults: true }); - expect(result.elicitation?.url).toBeUndefined(); - }); - - it('should not inject form capability when form is explicitly declared', () => { - const capabilities = { - elicitation: { - form: {} - } - }; - - const result = ClientCapabilitiesSchema.parse(capabilities); - expect(result.elicitation).toBeDefined(); - expect(result.elicitation?.form).toBeDefined(); - expect(result.elicitation?.form).toEqual({}); - expect(result.elicitation?.url).toBeUndefined(); - }); - - it('should not inject form capability when url is explicitly declared', () => { - const capabilities = { - elicitation: { - url: {} - } - }; - - const result = ClientCapabilitiesSchema.parse(capabilities); - expect(result.elicitation).toBeDefined(); - expect(result.elicitation?.url).toBeDefined(); - expect(result.elicitation?.url).toEqual({}); - expect(result.elicitation?.form).toBeUndefined(); - }); - - it('should not inject form capability when both form and url are explicitly declared', () => { - const capabilities = { - elicitation: { - form: {}, - url: {} - } - }; - - const result = ClientCapabilitiesSchema.parse(capabilities); - expect(result.elicitation).toBeDefined(); - expect(result.elicitation?.form).toBeDefined(); - expect(result.elicitation?.url).toBeDefined(); - expect(result.elicitation?.form).toEqual({}); - expect(result.elicitation?.url).toEqual({}); - }); - - it('should not inject form capability when elicitation is undefined', () => { - const capabilities = {}; - - const result = ClientCapabilitiesSchema.parse(capabilities); - // When elicitation is not provided, it should remain undefined - expect(result.elicitation).toBeUndefined(); - }); - - it('should work within InitializeRequestParamsSchema context', () => { - const initializeParams = { - protocolVersion: '2025-11-25', - capabilities: { - elicitation: {} - }, - clientInfo: { - name: 'test client', - version: '1.0' - } - }; - - const result = InitializeRequestParamsSchema.parse(initializeParams); - expect(result.capabilities.elicitation).toBeDefined(); - expect(result.capabilities.elicitation?.form).toBeDefined(); - expect(result.capabilities.elicitation?.form).toEqual({}); - }); - }); -}); diff --git a/packages/core/test/types.test.ts b/packages/core/test/types.test.ts deleted file mode 100644 index c6c2b5c413..0000000000 --- a/packages/core/test/types.test.ts +++ /dev/null @@ -1,1174 +0,0 @@ -import { - CallToolRequestSchema, - CallToolResultSchema, - CLIENT_CAPABILITIES_META_KEY, - CLIENT_INFO_META_KEY, - ClientCapabilitiesSchema, - ClientRequestSchema, - CompleteRequestSchema, - ContentBlockSchema, - CreateMessageRequestSchema, - CreateMessageResultSchema, - CreateMessageResultWithToolsSchema, - DiscoverRequestSchema, - DiscoverResultSchema, - ElicitRequestFormParamsSchema, - EmptyResultSchema, - LATEST_PROTOCOL_VERSION, - LOG_LEVEL_META_KEY, - PromptMessageSchema, - PROTOCOL_VERSION_META_KEY, - RequestMetaEnvelopeSchema, - ResourceLinkSchema, - ResultSchema, - SamplingMessageSchema, - SUPPORTED_PROTOCOL_VERSIONS, - ToolChoiceSchema, - ToolResultContentSchema, - ToolSchema, - ToolUseContentSchema -} from '../src/types/index'; - -describe('Types', () => { - test('should have correct latest protocol version', () => { - expect(LATEST_PROTOCOL_VERSION).toBeDefined(); - expect(LATEST_PROTOCOL_VERSION).toBe('2025-11-25'); - }); - test('should have correct supported protocol versions', () => { - expect(SUPPORTED_PROTOCOL_VERSIONS).toBeDefined(); - expect(SUPPORTED_PROTOCOL_VERSIONS).toBeInstanceOf(Array); - expect(SUPPORTED_PROTOCOL_VERSIONS).toContain(LATEST_PROTOCOL_VERSION); - expect(SUPPORTED_PROTOCOL_VERSIONS).toContain('2025-06-18'); - expect(SUPPORTED_PROTOCOL_VERSIONS).toContain('2025-03-26'); - expect(SUPPORTED_PROTOCOL_VERSIONS).toContain('2024-11-05'); - expect(SUPPORTED_PROTOCOL_VERSIONS).toContain('2024-10-07'); - }); - - describe('ResourceLink', () => { - test('should validate a minimal ResourceLink', () => { - const resourceLink = { - type: 'resource_link', - uri: 'file:///path/to/file.txt', - name: 'file.txt' - }; - - const result = ResourceLinkSchema.safeParse(resourceLink); - expect(result.success).toBe(true); - if (result.success) { - expect(result.data.type).toBe('resource_link'); - expect(result.data.uri).toBe('file:///path/to/file.txt'); - expect(result.data.name).toBe('file.txt'); - } - }); - - test('should validate a ResourceLink with all optional fields', () => { - const resourceLink = { - type: 'resource_link', - uri: 'https://example.com/resource', - name: 'Example Resource', - title: 'A comprehensive example resource', - description: 'This resource demonstrates all fields', - mimeType: 'text/plain', - _meta: { custom: 'metadata' } - }; - - const result = ResourceLinkSchema.safeParse(resourceLink); - expect(result.success).toBe(true); - if (result.success) { - expect(result.data.title).toBe('A comprehensive example resource'); - expect(result.data.description).toBe('This resource demonstrates all fields'); - expect(result.data.mimeType).toBe('text/plain'); - expect(result.data._meta).toEqual({ custom: 'metadata' }); - } - }); - - test('should fail validation for invalid type', () => { - const invalidResourceLink = { - type: 'invalid_type', - uri: 'file:///path/to/file.txt', - name: 'file.txt' - }; - - const result = ResourceLinkSchema.safeParse(invalidResourceLink); - expect(result.success).toBe(false); - }); - - test('should fail validation for missing required fields', () => { - const invalidResourceLink = { - type: 'resource_link', - uri: 'file:///path/to/file.txt' - // missing name - }; - - const result = ResourceLinkSchema.safeParse(invalidResourceLink); - expect(result.success).toBe(false); - }); - }); - - describe('ContentBlock', () => { - test('should validate text content', () => { - const mockDate = new Date().toISOString(); - const textContent = { - type: 'text', - text: 'Hello, world!', - annotations: { - audience: ['user'], - priority: 0.5, - lastModified: mockDate - } - }; - - const result = ContentBlockSchema.safeParse(textContent); - expect(result.success).toBe(true); - if (result.success) { - expect(result.data.type).toBe('text'); - expect(result.data.annotations).toEqual({ - audience: ['user'], - priority: 0.5, - lastModified: mockDate - }); - } - }); - - test('should validate image content', () => { - const mockDate = new Date().toISOString(); - const imageContent = { - type: 'image', - data: 'aGVsbG8=', // base64 encoded "hello" - mimeType: 'image/png', - annotations: { - audience: ['user'], - priority: 0.5, - lastModified: mockDate - } - }; - - const result = ContentBlockSchema.safeParse(imageContent); - expect(result.success).toBe(true); - if (result.success) { - expect(result.data.type).toBe('image'); - expect(result.data.annotations).toEqual({ - audience: ['user'], - priority: 0.5, - lastModified: mockDate - }); - } - }); - - test('should validate audio content', () => { - const mockDate = new Date().toISOString(); - const audioContent = { - type: 'audio', - data: 'aGVsbG8=', // base64 encoded "hello" - mimeType: 'audio/mp3', - annotations: { - audience: ['user'], - priority: 0.5, - lastModified: mockDate - } - }; - - const result = ContentBlockSchema.safeParse(audioContent); - expect(result.success).toBe(true); - if (result.success) { - expect(result.data.type).toBe('audio'); - expect(result.data.annotations).toEqual({ - audience: ['user'], - priority: 0.5, - lastModified: mockDate - }); - } - }); - - test('should validate resource link content', () => { - const mockDate = new Date().toISOString(); - const resourceLink = { - type: 'resource_link', - uri: 'file:///path/to/file.txt', - name: 'file.txt', - mimeType: 'text/plain', - annotations: { - audience: ['user'], - priority: 0.5, - lastModified: mockDate - } - }; - - const result = ContentBlockSchema.safeParse(resourceLink); - expect(result.success).toBe(true); - if (result.success) { - expect(result.data.type).toBe('resource_link'); - expect(result.data.annotations).toEqual({ - audience: ['user'], - priority: 0.5, - lastModified: mockDate - }); - } - }); - - test('should validate embedded resource content', () => { - const mockDate = new Date().toISOString(); - const embeddedResource = { - type: 'resource', - resource: { - uri: 'file:///path/to/file.txt', - mimeType: 'text/plain', - text: 'File contents' - }, - annotations: { - audience: ['user'], - priority: 0.5, - lastModified: mockDate - } - }; - - const result = ContentBlockSchema.safeParse(embeddedResource); - expect(result.success).toBe(true); - if (result.success) { - expect(result.data.type).toBe('resource'); - expect(result.data.annotations).toEqual({ - audience: ['user'], - priority: 0.5, - lastModified: mockDate - }); - } - }); - }); - - describe('PromptMessage with ContentBlock', () => { - test('should validate prompt message with resource link', () => { - const promptMessage = { - role: 'assistant', - content: { - type: 'resource_link', - uri: 'file:///project/src/main.rs', - name: 'main.rs', - description: 'Primary application entry point', - mimeType: 'text/x-rust' - } - }; - - const result = PromptMessageSchema.safeParse(promptMessage); - expect(result.success).toBe(true); - if (result.success) { - expect(result.data.content.type).toBe('resource_link'); - } - }); - }); - - describe('CallToolResult with ContentBlock', () => { - test('should validate tool result with resource links', () => { - const toolResult = { - content: [ - { - type: 'text', - text: 'Found the following files:' - }, - { - type: 'resource_link', - uri: 'file:///project/src/main.rs', - name: 'main.rs', - description: 'Primary application entry point', - mimeType: 'text/x-rust' - }, - { - type: 'resource_link', - uri: 'file:///project/src/lib.rs', - name: 'lib.rs', - description: 'Library exports', - mimeType: 'text/x-rust' - } - ] - }; - - const result = CallToolResultSchema.safeParse(toolResult); - expect(result.success).toBe(true); - if (result.success) { - expect(result.data.content).toHaveLength(3); - expect(result.data.content[0]?.type).toBe('text'); - expect(result.data.content[1]?.type).toBe('resource_link'); - expect(result.data.content[2]?.type).toBe('resource_link'); - } - }); - - test('should validate empty content array with default', () => { - const toolResult = {}; - - const result = CallToolResultSchema.safeParse(toolResult); - expect(result.success).toBe(true); - if (result.success) { - expect(result.data.content).toEqual([]); - } - }); - }); - - describe('CompleteRequest', () => { - test('should validate a CompleteRequest without resolved field', () => { - const request = { - method: 'completion/complete', - params: { - ref: { type: 'ref/prompt', name: 'greeting' }, - argument: { name: 'name', value: 'A' } - } - }; - - const result = CompleteRequestSchema.safeParse(request); - expect(result.success).toBe(true); - if (result.success) { - expect(result.data.method).toBe('completion/complete'); - expect(result.data.params.ref.type).toBe('ref/prompt'); - expect(result.data.params.context).toBeUndefined(); - } - }); - - test('should validate a CompleteRequest with resolved field', () => { - const request = { - method: 'completion/complete', - params: { - ref: { type: 'ref/resource', uri: 'github://repos/{owner}/{repo}' }, - argument: { name: 'repo', value: 't' }, - context: { - arguments: { - '{owner}': 'microsoft' - } - } - } - }; - - const result = CompleteRequestSchema.safeParse(request); - expect(result.success).toBe(true); - if (result.success) { - expect(result.data.params.context?.arguments).toEqual({ - '{owner}': 'microsoft' - }); - } - }); - - test('should validate a CompleteRequest with empty resolved field', () => { - const request = { - method: 'completion/complete', - params: { - ref: { type: 'ref/prompt', name: 'test' }, - argument: { name: 'arg', value: '' }, - context: { - arguments: {} - } - } - }; - - const result = CompleteRequestSchema.safeParse(request); - expect(result.success).toBe(true); - if (result.success) { - expect(result.data.params.context?.arguments).toEqual({}); - } - }); - - test('should validate a CompleteRequest with multiple resolved variables', () => { - const request = { - method: 'completion/complete', - params: { - ref: { type: 'ref/resource', uri: 'api://v1/{tenant}/{resource}/{id}' }, - argument: { name: 'id', value: '123' }, - context: { - arguments: { - '{tenant}': 'acme-corp', - '{resource}': 'users' - } - } - } - }; - - const result = CompleteRequestSchema.safeParse(request); - expect(result.success).toBe(true); - if (result.success) { - expect(result.data.params.context?.arguments).toEqual({ - '{tenant}': 'acme-corp', - '{resource}': 'users' - }); - } - }); - }); - - describe('ToolSchema - JSON Schema 2020-12 support', () => { - test('should accept inputSchema with $schema field', () => { - const tool = { - name: 'test', - inputSchema: { - $schema: 'https://json-schema.org/draft/2020-12/schema', - type: 'object', - properties: { name: { type: 'string' } } - } - }; - const result = ToolSchema.safeParse(tool); - expect(result.success).toBe(true); - }); - - test('should accept inputSchema with additionalProperties', () => { - const tool = { - name: 'test', - inputSchema: { - type: 'object', - properties: { name: { type: 'string' } }, - additionalProperties: false - } - }; - const result = ToolSchema.safeParse(tool); - expect(result.success).toBe(true); - }); - - test('should accept inputSchema with composition keywords', () => { - const tool = { - name: 'test', - inputSchema: { - type: 'object', - allOf: [{ properties: { a: { type: 'string' } } }, { properties: { b: { type: 'number' } } }] - } - }; - const result = ToolSchema.safeParse(tool); - expect(result.success).toBe(true); - }); - - test('should accept inputSchema with $ref and $defs', () => { - const tool = { - name: 'test', - inputSchema: { - type: 'object', - properties: { user: { $ref: '#/$defs/User' } }, - $defs: { - User: { type: 'object', properties: { name: { type: 'string' } } } - } - } - }; - const result = ToolSchema.safeParse(tool); - expect(result.success).toBe(true); - }); - - test('should accept inputSchema with metadata keywords', () => { - const tool = { - name: 'test', - inputSchema: { - type: 'object', - title: 'User Input', - description: 'Input parameters for user creation', - deprecated: false, - examples: [{ name: 'John' }], - properties: { name: { type: 'string' } } - } - }; - const result = ToolSchema.safeParse(tool); - expect(result.success).toBe(true); - }); - - test('should accept outputSchema with full JSON Schema features', () => { - const tool = { - name: 'test', - inputSchema: { type: 'object' }, - outputSchema: { - type: 'object', - properties: { - id: { type: 'string' }, - tags: { type: 'array' } - }, - required: ['id'], - additionalProperties: false, - minProperties: 1 - } - }; - const result = ToolSchema.safeParse(tool); - expect(result.success).toBe(true); - }); - - test('should still require type: object at root for inputSchema', () => { - const tool = { - name: 'test', - inputSchema: { - type: 'string' - } - }; - const result = ToolSchema.safeParse(tool); - expect(result.success).toBe(false); - }); - - test('should still require type: object at root for outputSchema', () => { - const tool = { - name: 'test', - inputSchema: { type: 'object' }, - outputSchema: { - type: 'array' - } - }; - const result = ToolSchema.safeParse(tool); - expect(result.success).toBe(false); - }); - - test('should accept simple minimal schema (backward compatibility)', () => { - const tool = { - name: 'test', - inputSchema: { - type: 'object', - properties: { name: { type: 'string' } }, - required: ['name'] - } - }; - const result = ToolSchema.safeParse(tool); - expect(result.success).toBe(true); - }); - }); - - describe('ToolUseContent', () => { - test('should validate a tool call content', () => { - const toolCall = { - type: 'tool_use', - id: 'call_123', - name: 'get_weather', - input: { city: 'San Francisco', units: 'celsius' } - }; - - const result = ToolUseContentSchema.safeParse(toolCall); - expect(result.success).toBe(true); - if (result.success) { - expect(result.data.type).toBe('tool_use'); - expect(result.data.id).toBe('call_123'); - expect(result.data.name).toBe('get_weather'); - expect(result.data.input).toEqual({ city: 'San Francisco', units: 'celsius' }); - } - }); - - test('should validate tool call with _meta', () => { - const toolCall = { - type: 'tool_use', - id: 'call_456', - name: 'search', - input: { query: 'test' }, - _meta: { custom: 'data' } - }; - - const result = ToolUseContentSchema.safeParse(toolCall); - expect(result.success).toBe(true); - if (result.success) { - expect(result.data._meta).toEqual({ custom: 'data' }); - } - }); - - test('should fail validation for missing required fields', () => { - const invalidToolCall = { - type: 'tool_use', - name: 'test' - // missing id and input - }; - - const result = ToolUseContentSchema.safeParse(invalidToolCall); - expect(result.success).toBe(false); - }); - }); - - describe('ToolResultContent', () => { - test('should validate a tool result content', () => { - const toolResult = { - type: 'tool_result', - toolUseId: 'call_123', - structuredContent: { temperature: 72, condition: 'sunny' } - }; - - const result = ToolResultContentSchema.safeParse(toolResult); - expect(result.success).toBe(true); - if (result.success) { - expect(result.data.type).toBe('tool_result'); - expect(result.data.toolUseId).toBe('call_123'); - expect(result.data.structuredContent).toEqual({ temperature: 72, condition: 'sunny' }); - } - }); - - test('should validate tool result with error in content', () => { - const toolResult = { - type: 'tool_result', - toolUseId: 'call_456', - structuredContent: { error: 'API_ERROR', message: 'Service unavailable' }, - isError: true - }; - - const result = ToolResultContentSchema.safeParse(toolResult); - expect(result.success).toBe(true); - if (result.success) { - expect(result.data.structuredContent).toEqual({ error: 'API_ERROR', message: 'Service unavailable' }); - expect(result.data.isError).toBe(true); - } - }); - - test('should fail validation for missing required fields', () => { - const invalidToolResult = { - type: 'tool_result', - content: { data: 'test' } - // missing toolUseId - }; - - const result = ToolResultContentSchema.safeParse(invalidToolResult); - expect(result.success).toBe(false); - }); - }); - - describe('ToolChoice', () => { - test('should validate tool choice with mode auto', () => { - const toolChoice = { - mode: 'auto' - }; - - const result = ToolChoiceSchema.safeParse(toolChoice); - expect(result.success).toBe(true); - if (result.success) { - expect(result.data.mode).toBe('auto'); - } - }); - - test('should validate tool choice with mode required', () => { - const toolChoice = { - mode: 'required' - }; - - const result = ToolChoiceSchema.safeParse(toolChoice); - expect(result.success).toBe(true); - if (result.success) { - expect(result.data.mode).toBe('required'); - } - }); - - test('should validate empty tool choice', () => { - const toolChoice = {}; - - const result = ToolChoiceSchema.safeParse(toolChoice); - expect(result.success).toBe(true); - }); - - test('should fail validation for invalid mode', () => { - const invalidToolChoice = { - mode: 'invalid' - }; - - const result = ToolChoiceSchema.safeParse(invalidToolChoice); - expect(result.success).toBe(false); - }); - }); - - describe('SamplingMessage content types', () => { - test('should validate user message with text', () => { - const userMessage = { - role: 'user', - content: { type: 'text', text: "What's the weather?" } - }; - - const result = SamplingMessageSchema.safeParse(userMessage); - expect(result.success).toBe(true); - if (result.success) { - expect(result.data.role).toBe('user'); - if (!Array.isArray(result.data.content)) { - expect(result.data.content.type).toBe('text'); - } - } - }); - - test('should validate user message with tool result', () => { - const userMessage = { - role: 'user', - content: { - type: 'tool_result', - toolUseId: 'call_123', - content: [] - } - }; - - const result = SamplingMessageSchema.safeParse(userMessage); - expect(result.success).toBe(true); - if (result.success && !Array.isArray(result.data.content)) { - expect(result.data.content.type).toBe('tool_result'); - } - }); - - test('should validate assistant message with text', () => { - const assistantMessage = { - role: 'assistant', - content: { type: 'text', text: "I'll check the weather for you." } - }; - - const result = SamplingMessageSchema.safeParse(assistantMessage); - expect(result.success).toBe(true); - if (result.success) { - expect(result.data.role).toBe('assistant'); - } - }); - - test('should validate assistant message with tool call', () => { - const assistantMessage = { - role: 'assistant', - content: { - type: 'tool_use', - id: 'call_123', - name: 'get_weather', - input: { city: 'SF' } - } - }; - - const result = SamplingMessageSchema.safeParse(assistantMessage); - expect(result.success).toBe(true); - if (result.success && !Array.isArray(result.data.content)) { - expect(result.data.content.type).toBe('tool_use'); - } - }); - - test('should validate any content type for any role', () => { - // The simplified schema allows any content type for any role - const assistantWithToolResult = { - role: 'assistant', - content: { - type: 'tool_result', - toolUseId: 'call_123', - content: [] - } - }; - - const result1 = SamplingMessageSchema.safeParse(assistantWithToolResult); - expect(result1.success).toBe(true); - - const userWithToolUse = { - role: 'user', - content: { - type: 'tool_use', - id: 'call_123', - name: 'test', - input: {} - } - }; - - const result2 = SamplingMessageSchema.safeParse(userWithToolUse); - expect(result2.success).toBe(true); - }); - }); - - describe('SamplingMessage', () => { - test('should validate user message via discriminated union', () => { - const message = { - role: 'user', - content: { type: 'text', text: 'Hello' } - }; - - const result = SamplingMessageSchema.safeParse(message); - expect(result.success).toBe(true); - if (result.success) { - expect(result.data.role).toBe('user'); - } - }); - - test('should validate assistant message via discriminated union', () => { - const message = { - role: 'assistant', - content: { type: 'text', text: 'Hi there!' } - }; - - const result = SamplingMessageSchema.safeParse(message); - expect(result.success).toBe(true); - if (result.success) { - expect(result.data.role).toBe('assistant'); - } - }); - }); - - describe('CreateMessageRequest', () => { - test('should validate request without tools', () => { - const request = { - method: 'sampling/createMessage', - params: { - messages: [{ role: 'user', content: { type: 'text', text: 'Hello' } }], - maxTokens: 1000 - } - }; - - const result = CreateMessageRequestSchema.safeParse(request); - expect(result.success).toBe(true); - if (result.success) { - expect(result.data.params.tools).toBeUndefined(); - } - }); - - test('should validate request with tools', () => { - const request = { - method: 'sampling/createMessage', - params: { - messages: [{ role: 'user', content: { type: 'text', text: "What's the weather?" } }], - maxTokens: 1000, - tools: [ - { - name: 'get_weather', - description: 'Get weather for a location', - inputSchema: { - type: 'object', - properties: { - location: { type: 'string' } - }, - required: ['location'] - } - } - ], - toolChoice: { - mode: 'auto' - } - } - }; - - const result = CreateMessageRequestSchema.safeParse(request); - expect(result.success).toBe(true); - if (result.success) { - expect(result.data.params.tools).toHaveLength(1); - expect(result.data.params.toolChoice?.mode).toBe('auto'); - } - }); - - test('should validate request with includeContext (soft-deprecated)', () => { - const request = { - method: 'sampling/createMessage', - params: { - messages: [{ role: 'user', content: { type: 'text', text: 'Help' } }], - maxTokens: 1000, - includeContext: 'thisServer' - } - }; - - const result = CreateMessageRequestSchema.safeParse(request); - expect(result.success).toBe(true); - if (result.success) { - expect(result.data.params.includeContext).toBe('thisServer'); - } - }); - }); - - describe('CreateMessageResult', () => { - test('should validate result with text content', () => { - const result = { - model: 'claude-3-5-sonnet-20241022', - role: 'assistant', - content: { type: 'text', text: "Here's the answer." }, - stopReason: 'endTurn' - }; - - const parseResult = CreateMessageResultSchema.safeParse(result); - expect(parseResult.success).toBe(true); - if (parseResult.success) { - expect(parseResult.data.role).toBe('assistant'); - expect(parseResult.data.stopReason).toBe('endTurn'); - } - }); - - test('should validate result with tool call (using WithTools schema)', () => { - const result = { - model: 'claude-3-5-sonnet-20241022', - role: 'assistant', - content: { - type: 'tool_use', - id: 'call_123', - name: 'get_weather', - input: { city: 'SF' } - }, - stopReason: 'toolUse' - }; - - // Tool call results use CreateMessageResultWithToolsSchema - const parseResult = CreateMessageResultWithToolsSchema.safeParse(result); - expect(parseResult.success).toBe(true); - if (parseResult.success) { - expect(parseResult.data.stopReason).toBe('toolUse'); - const content = parseResult.data.content; - expect(Array.isArray(content)).toBe(false); - if (!Array.isArray(content)) { - expect(content.type).toBe('tool_use'); - } - } - - // Basic CreateMessageResultSchema should NOT accept tool_use content - const basicResult = CreateMessageResultSchema.safeParse(result); - expect(basicResult.success).toBe(false); - }); - - test('should validate result with array content (using WithTools schema)', () => { - const result = { - model: 'claude-3-5-sonnet-20241022', - role: 'assistant', - content: [ - { type: 'text', text: 'Let me check the weather.' }, - { - type: 'tool_use', - id: 'call_123', - name: 'get_weather', - input: { city: 'SF' } - } - ], - stopReason: 'toolUse' - }; - - // Array content uses CreateMessageResultWithToolsSchema - const parseResult = CreateMessageResultWithToolsSchema.safeParse(result); - expect(parseResult.success).toBe(true); - if (parseResult.success) { - expect(parseResult.data.stopReason).toBe('toolUse'); - const content = parseResult.data.content; - expect(Array.isArray(content)).toBe(true); - if (Array.isArray(content)) { - expect(content).toHaveLength(2); - expect(content[0]?.type).toBe('text'); - expect(content[1]?.type).toBe('tool_use'); - } - } - - // Basic CreateMessageResultSchema should NOT accept array content - const basicResult = CreateMessageResultSchema.safeParse(result); - expect(basicResult.success).toBe(false); - }); - - test('should validate all new stop reasons', () => { - const stopReasons = ['endTurn', 'stopSequence', 'maxTokens', 'toolUse', 'refusal', 'other']; - - for (const stopReason of stopReasons) { - const result = { - model: 'test', - role: 'assistant', - content: { type: 'text', text: 'test' }, - stopReason - }; - - const parseResult = CreateMessageResultSchema.safeParse(result); - expect(parseResult.success).toBe(true); - } - }); - - test('should allow custom stop reason string', () => { - const result = { - model: 'test', - role: 'assistant', - content: { type: 'text', text: 'test' }, - stopReason: 'custom_provider_reason' - }; - - const parseResult = CreateMessageResultSchema.safeParse(result); - expect(parseResult.success).toBe(true); - }); - }); - - describe('ClientCapabilities with sampling', () => { - test('should validate capabilities with sampling.tools', () => { - const capabilities = { - sampling: { - tools: {} - } - }; - - const result = ClientCapabilitiesSchema.safeParse(capabilities); - expect(result.success).toBe(true); - if (result.success) { - expect(result.data.sampling?.tools).toBeDefined(); - } - }); - - test('should validate capabilities with sampling.context', () => { - const capabilities = { - sampling: { - context: {} - } - }; - - const result = ClientCapabilitiesSchema.safeParse(capabilities); - expect(result.success).toBe(true); - if (result.success) { - expect(result.data.sampling?.context).toBeDefined(); - } - }); - - test('should validate capabilities with both', () => { - const capabilities = { - sampling: { - context: {}, - tools: {} - } - }; - - const result = ClientCapabilitiesSchema.safeParse(capabilities); - expect(result.success).toBe(true); - if (result.success) { - expect(result.data.sampling?.context).toBeDefined(); - expect(result.data.sampling?.tools).toBeDefined(); - } - }); - }); - - describe('ElicitRequestFormParamsSchema', () => { - test('accepts requestedSchema with extra JSON Schema metadata keys', () => { - // Mirrors what z.toJSONSchema() emits — includes $schema, additionalProperties, etc. - // See https://github.com/modelcontextprotocol/typescript-sdk/issues/1362 - const params = { - message: 'Please provide your name', - requestedSchema: { - $schema: 'https://json-schema.org/draft/2020-12/schema', - type: 'object', - properties: { - name: { type: 'string' } - }, - required: ['name'], - additionalProperties: false - } - }; - - const result = ElicitRequestFormParamsSchema.safeParse(params); - expect(result.success).toBe(true); - if (result.success) { - expect(result.data.requestedSchema.type).toBe('object'); - expect(result.data.requestedSchema.$schema).toBe('https://json-schema.org/draft/2020-12/schema'); - expect(result.data.requestedSchema.additionalProperties).toBe(false); - } - }); - }); -}); - -describe('2025-11-25 task wire interop (task feature removed; wire types remain)', () => { - test('tasks/get parses through the client request union', () => { - const result = ClientRequestSchema.safeParse({ method: 'tasks/get', params: { taskId: 'task-123' } }); - expect(result.success).toBe(true); - }); - - test('task-augmented tools/call params parse and retain the task field', () => { - const result = CallToolRequestSchema.safeParse({ - method: 'tools/call', - params: { name: 'echo', arguments: {}, task: { ttl: 60000 } } - }); - expect(result.success).toBe(true); - if (result.success) { - expect(result.data.params.task).toEqual({ ttl: 60000 }); - } - }); - - test('tool execution.taskSupport is accepted', () => { - const result = ToolSchema.safeParse({ - name: 'echo', - inputSchema: { type: 'object' }, - execution: { taskSupport: 'optional' } - }); - expect(result.success).toBe(true); - }); - - test('capabilities.tasks is retained, not stripped', () => { - const result = ClientCapabilitiesSchema.safeParse({ tasks: { list: {}, cancel: {} } }); - expect(result.success).toBe(true); - if (result.success) { - expect(result.data.tasks).toEqual({ list: {}, cancel: {} }); - } - }); -}); - -describe('2026-07-28 wire shapes', () => { - describe('RequestMetaEnvelope', () => { - const envelope = { - [PROTOCOL_VERSION_META_KEY]: '2026-07-28', - [CLIENT_INFO_META_KEY]: { name: 'test-client', version: '1.0.0' }, - [CLIENT_CAPABILITIES_META_KEY]: {} - }; - - test('accepts a complete envelope', () => { - const result = RequestMetaEnvelopeSchema.safeParse(envelope); - expect(result.success).toBe(true); - if (result.success) { - expect(result.data[PROTOCOL_VERSION_META_KEY]).toBe('2026-07-28'); - expect(result.data[CLIENT_INFO_META_KEY]).toEqual({ name: 'test-client', version: '1.0.0' }); - expect(result.data[CLIENT_CAPABILITIES_META_KEY]).toEqual({}); - } - }); - - test('accepts the optional log level, progress token, and unknown keys', () => { - const result = RequestMetaEnvelopeSchema.safeParse({ - ...envelope, - [LOG_LEVEL_META_KEY]: 'warning', - progressToken: 'token-1', - 'com.example/custom': { anything: true } - }); - expect(result.success).toBe(true); - if (result.success) { - expect(result.data[LOG_LEVEL_META_KEY]).toBe('warning'); - expect(result.data.progressToken).toBe('token-1'); - expect(result.data['com.example/custom']).toEqual({ anything: true }); - } - }); - - test.each([PROTOCOL_VERSION_META_KEY, CLIENT_INFO_META_KEY, CLIENT_CAPABILITIES_META_KEY])( - 'rejects an envelope missing %s', - key => { - const incomplete: Record = { ...envelope }; - delete incomplete[key]; - expect(RequestMetaEnvelopeSchema.safeParse(incomplete).success).toBe(false); - } - ); - - test('rejects an invalid log level', () => { - const result = RequestMetaEnvelopeSchema.safeParse({ ...envelope, [LOG_LEVEL_META_KEY]: 'loud' }); - expect(result.success).toBe(false); - }); - }); - - describe('DiscoverRequest', () => { - test('parses a discover request with and without params', () => { - expect(DiscoverRequestSchema.safeParse({ method: 'server/discover' }).success).toBe(true); - expect( - DiscoverRequestSchema.safeParse({ - method: 'server/discover', - params: { _meta: { [PROTOCOL_VERSION_META_KEY]: '2026-07-28' } } - }).success - ).toBe(true); - }); - - test('rejects other methods', () => { - expect(DiscoverRequestSchema.safeParse({ method: 'initialize' }).success).toBe(false); - }); - }); - - describe('DiscoverResult', () => { - const result = { - supportedVersions: ['2026-07-28'], - capabilities: { tools: { listChanged: true } }, - serverInfo: { name: 'test-server', version: '1.0.0' } - }; - - test('parses a discover result', () => { - const parsed = DiscoverResultSchema.safeParse({ ...result, resultType: 'complete', instructions: 'Use the echo tool.' }); - expect(parsed.success).toBe(true); - if (parsed.success) { - expect(parsed.data.supportedVersions).toEqual(['2026-07-28']); - expect(parsed.data.capabilities).toEqual({ tools: { listChanged: true } }); - expect(parsed.data.serverInfo).toEqual({ name: 'test-server', version: '1.0.0' }); - expect(parsed.data.instructions).toBe('Use the echo tool.'); - } - }); - - test.each(['supportedVersions', 'capabilities', 'serverInfo'])('rejects a discover result missing %s', key => { - const incomplete: Record = { ...result }; - delete incomplete[key]; - expect(DiscoverResultSchema.safeParse(incomplete).success).toBe(false); - }); - }); - - describe('Result resultType passthrough', () => { - test('accepts results with and without resultType (absent means "complete")', () => { - const withIt = ResultSchema.safeParse({ resultType: 'complete' }); - expect(withIt.success).toBe(true); - if (withIt.success) { - expect(withIt.data.resultType).toBe('complete'); - } - const withoutIt = ResultSchema.safeParse({}); - expect(withoutIt.success).toBe(true); - if (withoutIt.success) { - expect(withoutIt.data.resultType).toBeUndefined(); - } - }); - - test('rejects a non-string resultType', () => { - expect(ResultSchema.safeParse({ resultType: 42 }).success).toBe(false); - }); - - test('EmptyResult accepts resultType but still rejects unknown keys', () => { - expect(EmptyResultSchema.safeParse({ resultType: 'complete' }).success).toBe(true); - expect(EmptyResultSchema.safeParse({ unexpected: true }).success).toBe(false); - }); - }); -}); diff --git a/packages/core/test/types/errors.test.ts b/packages/core/test/types/errors.test.ts deleted file mode 100644 index 1072537d97..0000000000 --- a/packages/core/test/types/errors.test.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { describe, expect, it } from 'vitest'; - -import { ProtocolErrorCode } from '../../src/types/enums'; -import { ProtocolError, UnsupportedProtocolVersionError } from '../../src/types/errors'; - -describe('UnsupportedProtocolVersionError', () => { - const data = { supported: ['2025-11-25', '2025-06-18'], requested: '2026-07-28' }; - - it('carries code -32004 and the supported/requested data', () => { - const error = new UnsupportedProtocolVersionError(data); - expect(error.code).toBe(ProtocolErrorCode.UnsupportedProtocolVersion); - expect(error.code).toBe(-32004); - expect(error.supported).toEqual(['2025-11-25', '2025-06-18']); - expect(error.requested).toBe('2026-07-28'); - expect(error.data).toEqual(data); - }); - - it('defaults the message from the requested version', () => { - const error = new UnsupportedProtocolVersionError(data); - expect(error.message).toBe('Unsupported protocol version: 2026-07-28'); - const custom = new UnsupportedProtocolVersionError(data, 'try another version'); - expect(custom.message).toBe('try another version'); - }); - - it('is materialized by ProtocolError.fromError', () => { - const error = ProtocolError.fromError(-32004, 'Unsupported protocol version: 2026-07-28', data); - expect(error).toBeInstanceOf(UnsupportedProtocolVersionError); - if (error instanceof UnsupportedProtocolVersionError) { - expect(error.supported).toEqual(['2025-11-25', '2025-06-18']); - expect(error.requested).toBe('2026-07-28'); - } - expect(error.message).toBe('Unsupported protocol version: 2026-07-28'); - }); - - it('falls back to a generic ProtocolError when the data is missing or malformed', () => { - for (const malformed of [undefined, {}, { supported: 'not-an-array', requested: '2026-07-28' }, { supported: ['2025-11-25'] }]) { - const error = ProtocolError.fromError(-32004, 'unsupported', malformed); - expect(error).toBeInstanceOf(ProtocolError); - expect(error).not.toBeInstanceOf(UnsupportedProtocolVersionError); - expect(error.code).toBe(-32004); - expect(error.data).toEqual(malformed); - } - }); -}); diff --git a/packages/core/test/types/guards.test.ts b/packages/core/test/types/guards.test.ts deleted file mode 100644 index fe96b64dd3..0000000000 --- a/packages/core/test/types/guards.test.ts +++ /dev/null @@ -1,123 +0,0 @@ -import { describe, expect, it } from 'vitest'; - -import { JSONRPC_VERSION } from '../../src/types/constants'; -import { isCallToolResult, isJSONRPCErrorResponse, isJSONRPCResponse, isJSONRPCResultResponse } from '../../src/types/guards'; - -describe('isJSONRPCResponse', () => { - it('returns true for a valid result response', () => { - expect( - isJSONRPCResponse({ - jsonrpc: JSONRPC_VERSION, - id: 1, - result: {} - }) - ).toBe(true); - }); - - it('returns true for a valid error response', () => { - expect( - isJSONRPCResponse({ - jsonrpc: JSONRPC_VERSION, - id: 1, - error: { code: -32_600, message: 'Invalid Request' } - }) - ).toBe(true); - }); - - it('returns false for a request', () => { - expect( - isJSONRPCResponse({ - jsonrpc: JSONRPC_VERSION, - id: 1, - method: 'test' - }) - ).toBe(false); - }); - - it('returns false for a notification', () => { - expect( - isJSONRPCResponse({ - jsonrpc: JSONRPC_VERSION, - method: 'test' - }) - ).toBe(false); - }); - - it('returns false for arbitrary objects', () => { - expect(isJSONRPCResponse({ foo: 'bar' })).toBe(false); - }); - - it('narrows the type correctly', () => { - const value: unknown = { - jsonrpc: JSONRPC_VERSION, - id: 1, - result: { content: [] } - }; - if (isJSONRPCResponse(value)) { - // Type should be narrowed to JSONRPCResponse - expect(value.jsonrpc).toBe(JSONRPC_VERSION); - expect(value.id).toBe(1); - } - }); - - it('agrees with isJSONRPCResultResponse || isJSONRPCErrorResponse', () => { - const values = [ - { jsonrpc: JSONRPC_VERSION, id: 1, result: {} }, - { jsonrpc: JSONRPC_VERSION, id: 2, error: { code: -1, message: 'err' } }, - { jsonrpc: JSONRPC_VERSION, id: 3, method: 'test' }, - { jsonrpc: JSONRPC_VERSION, method: 'notify' }, - { foo: 'bar' }, - null, - 42 - ]; - for (const v of values) { - expect(isJSONRPCResponse(v)).toBe(isJSONRPCResultResponse(v) || isJSONRPCErrorResponse(v)); - } - }); -}); - -describe('isCallToolResult', () => { - it('returns false for an empty object (content is required)', () => { - expect(isCallToolResult({})).toBe(false); - }); - - it('returns true for a result with content', () => { - expect( - isCallToolResult({ - content: [{ type: 'text', text: 'hello' }] - }) - ).toBe(true); - }); - - it('returns true for a result with isError', () => { - expect( - isCallToolResult({ - content: [{ type: 'text', text: 'fail' }], - isError: true - }) - ).toBe(true); - }); - - it('returns true for a result with structuredContent', () => { - expect( - isCallToolResult({ - content: [], - structuredContent: { key: 'value' } - }) - ).toBe(true); - }); - - it('returns false for non-objects', () => { - expect(isCallToolResult(null)).toBe(false); - expect(isCallToolResult(42)).toBe(false); - expect(isCallToolResult('string')).toBe(false); - }); - - it('returns false for invalid content items', () => { - expect( - isCallToolResult({ - content: [{ type: 'invalid' }] - }) - ).toBe(false); - }); -}); diff --git a/packages/core/test/types/specTypeSchema.test.ts b/packages/core/test/types/specTypeSchema.test.ts deleted file mode 100644 index e04e9f1c45..0000000000 --- a/packages/core/test/types/specTypeSchema.test.ts +++ /dev/null @@ -1,177 +0,0 @@ -import { describe, expect, expectTypeOf, it } from 'vitest'; - -import type { OAuthMetadata, OAuthTokens } from '../../src/shared/auth'; -import * as schemas from '../../src/types/schemas'; -import type { SpecTypeName, SpecTypes } from '../../src/types/specTypeSchema'; -import { isSpecType, specTypeSchemas } from '../../src/types/specTypeSchema'; -import type { - CallToolResult, - ContentBlock, - Implementation, - JSONObject, - JSONRPCRequest, - JSONValue, - ResourceTemplateType, - Tool -} from '../../src/types/types'; - -describe('specTypeSchemas', () => { - it('returns a StandardSchemaV1Sync validator that accepts valid values', () => { - const result = specTypeSchemas.Implementation['~standard'].validate({ name: 'x', version: '1.0.0' }); - expect(result.issues).toBeUndefined(); - }); - - it('returns a validator that rejects invalid values with issues', () => { - const result = specTypeSchemas.Implementation['~standard'].validate({ name: 'x' }); - expect(result.issues?.length).toBeGreaterThan(0); - }); - - it('rejects unknown names at compile time and is undefined at runtime', () => { - // @ts-expect-error - 'NotASpecType' is not a SpecTypeName - expect(specTypeSchemas['NotASpecType']).toBeUndefined(); - }); - - it('covers JSON-RPC envelope types', () => { - const ok = specTypeSchemas.JSONRPCRequest['~standard'].validate({ jsonrpc: '2.0', id: 1, method: 'ping' }); - expect(ok.issues).toBeUndefined(); - }); - - it('covers OAuth types from shared/auth.ts', () => { - const ok = specTypeSchemas.OAuthTokens['~standard'].validate({ access_token: 'x', token_type: 'Bearer' }); - expect(ok.issues).toBeUndefined(); - const bad = specTypeSchemas.OAuthTokens['~standard'].validate({ token_type: 'Bearer' }); - expect(bad.issues?.length).toBeGreaterThan(0); - }); -}); - -describe('isSpecType', () => { - it('CallToolResult — accepts valid, rejects invalid/null/primitive', () => { - expect(isSpecType.CallToolResult({ content: [{ type: 'text', text: 'hi' }] })).toBe(true); - expect(isSpecType.CallToolResult({ content: 'not-an-array' })).toBe(false); - expect(isSpecType.CallToolResult(null)).toBe(false); - expect(isSpecType.CallToolResult('string')).toBe(false); - }); - - it('ContentBlock — accepts text block, rejects wrong shape', () => { - expect(isSpecType.ContentBlock({ type: 'text', text: 'hi' })).toBe(true); - expect(isSpecType.ContentBlock({ type: 'text' })).toBe(false); - expect(isSpecType.ContentBlock({})).toBe(false); - }); - - it('Tool — accepts valid, rejects missing inputSchema', () => { - expect(isSpecType.Tool({ name: 'echo', inputSchema: { type: 'object' } })).toBe(true); - expect(isSpecType.Tool({ name: 'echo' })).toBe(false); - }); - - it('ResourceTemplate — accepts valid, rejects missing uriTemplate', () => { - expect(isSpecType.ResourceTemplate({ name: 'r', uriTemplate: 'file:///{path}' })).toBe(true); - expect(isSpecType.ResourceTemplate({ name: 'r' })).toBe(false); - }); - - it('rejects unknown names at compile time and is undefined at runtime', () => { - // @ts-expect-error - 'NotASpecType' is not a SpecTypeName - expect(isSpecType['NotASpecType']).toBeUndefined(); - }); - - it('excludes internal helper schemas (no matching public type)', () => { - // @ts-expect-error - ListChangedOptionsBase is internal-only - expect(isSpecType['ListChangedOptionsBase']).toBeUndefined(); - // @ts-expect-error - BaseRequestParams is internal-only - expect(specTypeSchemas['BaseRequestParams']).toBeUndefined(); - // @ts-expect-error - NotificationsParams is internal-only - expect(isSpecType['NotificationsParams']).toBeUndefined(); - }); - - it('narrows the value type to the schema input type', () => { - const v: unknown = { name: 'x', version: '1.0.0' }; - if (isSpecType.Implementation(v)) { - // ImplementationSchema has no defaults/transforms, so its input type equals Implementation. - expectTypeOf(v).toEqualTypeOf(); - } - }); - - it('narrows to the input type, not the output type, for schemas with defaults', () => { - const v: unknown = {}; - expect(isSpecType.CallToolResult(v)).toBe(true); - if (isSpecType.CallToolResult(v)) { - // CallToolResultSchema has `content: z.array(...).default([])`, so the input type - // permits `content` to be absent. The guard narrows to that input shape. - expectTypeOf(v.content).toEqualTypeOf(); - expectTypeOf(v).not.toEqualTypeOf(); - } - }); - - it('JSONValue / JSONObject — narrows to the JSON type, not unknown', () => { - // These schemas use an explicit z.ZodType annotation for recursion; without the - // second param Zod's Input defaults to `unknown` and the predicate would not narrow. - const v: unknown = { a: 1 }; - if (isSpecType.JSONValue(v)) { - expectTypeOf(v).toEqualTypeOf(); - } - if (isSpecType.JSONObject(v)) { - expectTypeOf(v).toEqualTypeOf(); - } - }); - - it('guards work as filter callbacks and narrow the element type', () => { - const mixed: unknown[] = [{ type: 'text', text: 'hi' }, 42, { type: 'text' }]; - const blocks = mixed.filter(isSpecType.ContentBlock); - expect(blocks).toHaveLength(1); - expectTypeOf(blocks).toEqualTypeOf(); - }); -}); - -describe('SpecTypeName / SpecTypes (type-level)', () => { - it('SpecTypeName includes representative names', () => { - expectTypeOf<'CallToolResult'>().toMatchTypeOf(); - expectTypeOf<'ContentBlock'>().toMatchTypeOf(); - expectTypeOf<'Tool'>().toMatchTypeOf(); - expectTypeOf<'Implementation'>().toMatchTypeOf(); - expectTypeOf<'JSONRPCRequest'>().toMatchTypeOf(); - expectTypeOf<'OAuthTokens'>().toMatchTypeOf(); - expectTypeOf<'OAuthMetadata'>().toMatchTypeOf(); - expectTypeOf<'ResourceTemplate'>().toMatchTypeOf(); - }); - - it('SpecTypes[K] matches the named export type', () => { - expectTypeOf().toEqualTypeOf(); - expectTypeOf().toEqualTypeOf(); - expectTypeOf().toEqualTypeOf(); - expectTypeOf().toEqualTypeOf(); - expectTypeOf().toEqualTypeOf(); - expectTypeOf().toEqualTypeOf(); - expectTypeOf().toEqualTypeOf(); - // The public type is exported as ResourceTemplateType (the bare name collides with the - // server package's ResourceTemplate class), so this is the one entry where the key and - // the public type name differ. - expectTypeOf().toEqualTypeOf(); - }); -}); - -describe('SPEC_SCHEMA_KEYS allowlist', () => { - // Mirrors the exclusion comment in specTypeSchema.ts. If this list grows, confirm the new - // entry has no public type in types.ts before adding it here; otherwise add it to the allowlist. - const INTERNAL_HELPER_SCHEMAS: readonly string[] = [ - 'ListChangedOptionsBaseSchema', - 'BaseRequestParamsSchema', - 'NotificationsParamsSchema', - 'ClientTasksCapabilitySchema', - 'ServerTasksCapabilitySchema' - ]; - - it('covers every public protocol schema in schemas.ts (drift guard)', () => { - // PascalCase filters out helper functions like getRequestSchema/getResultSchema. - const allProtocolSchemas = Object.keys(schemas).filter(k => k.endsWith('Schema') && /^[A-Z]/.test(k)); - const expected = allProtocolSchemas - .filter(k => !INTERNAL_HELPER_SCHEMAS.includes(k)) - .map(k => k.slice(0, -'Schema'.length)) - .sort(); - // Auth schemas are sourced from shared/auth.ts, not schemas.ts. Keep only the protocol entries - // (whose `*Schema` const lives in schemas.ts) so the comparison stays against schemas.ts — - // robust to new auth schemas (e.g. IdJagTokenExchangeResponse) without a name-prefix heuristic. - const actual = Object.keys(isSpecType) - .filter(k => `${k}Schema` in schemas) - .sort(); - expect(actual).toEqual(expected); - }); -}); diff --git a/packages/core/test/util/standardSchema.test.ts b/packages/core/test/util/standardSchema.test.ts deleted file mode 100644 index 8856592ff0..0000000000 --- a/packages/core/test/util/standardSchema.test.ts +++ /dev/null @@ -1,42 +0,0 @@ -import * as z from 'zod/v4'; - -import { standardSchemaToJsonSchema } from '../../src/util/standardSchema'; - -describe('standardSchemaToJsonSchema', () => { - test('emits type:object for plain z.object schemas', () => { - const schema = z.object({ name: z.string(), age: z.number() }); - const result = standardSchemaToJsonSchema(schema, 'input'); - - expect(result.type).toBe('object'); - expect(result.properties).toBeDefined(); - }); - - test('emits type:object for discriminated unions', () => { - const schema = z.discriminatedUnion('action', [ - z.object({ action: z.literal('create'), name: z.string() }), - z.object({ action: z.literal('delete'), id: z.string() }) - ]); - const result = standardSchemaToJsonSchema(schema, 'input'); - - expect(result.type).toBe('object'); - // Zod emits oneOf for discriminated unions; the catchall on Tool.inputSchema - // accepts it, but the top-level type must be present per MCP spec. - expect(result.oneOf ?? result.anyOf).toBeDefined(); - }); - - test('throws for schemas with explicit non-object type', () => { - expect(() => standardSchemaToJsonSchema(z.string(), 'input')).toThrow(/must describe objects/); - expect(() => standardSchemaToJsonSchema(z.array(z.string()), 'input')).toThrow(/must describe objects/); - expect(() => standardSchemaToJsonSchema(z.number(), 'input')).toThrow(/must describe objects/); - }); - - test('preserves existing type:object without modification', () => { - const schema = z.object({ x: z.string() }); - const result = standardSchemaToJsonSchema(schema, 'input'); - - // Spread order means zod's own type:"object" wins; verify no double-wrap. - const keys = Object.keys(result); - expect(keys.filter(k => k === 'type')).toHaveLength(1); - expect(result.type).toBe('object'); - }); -}); diff --git a/packages/core/test/util/standardSchema.zodFallback.test.ts b/packages/core/test/util/standardSchema.zodFallback.test.ts deleted file mode 100644 index f8862b08a3..0000000000 --- a/packages/core/test/util/standardSchema.zodFallback.test.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { describe, expect, it, vi } from 'vitest'; -import * as z from 'zod/v4'; -import { standardSchemaToJsonSchema } from '../../src/util/standardSchema'; - -type SchemaArg = Parameters[0]; - -describe('standardSchemaToJsonSchema — zod fallback paths', () => { - it('falls back to z.toJSONSchema for zod 4.0–4.1 (vendor=zod, no ~standard.jsonSchema, has _zod)', () => { - const warn = vi.spyOn(console, 'warn').mockImplementation(() => {}); - const real = z.object({ a: z.string() }); - // Simulate zod 4.0–4.1: shadow `~standard` on the real instance with `jsonSchema` removed. - // Keeps the rest of the zod 4 object (including `_zod`) intact so z.toJSONSchema can introspect it. - const { jsonSchema: _drop, ...stdNoJson } = real['~standard'] as unknown as Record; - void _drop; - Object.defineProperty(real, '~standard', { value: { ...stdNoJson, vendor: 'zod' }, configurable: true }); - - const result = standardSchemaToJsonSchema(real as unknown as SchemaArg); - expect(result.type).toBe('object'); - expect((result.properties as unknown as Record)?.a).toBeDefined(); - expect(warn).toHaveBeenCalledOnce(); - expect(warn.mock.calls[0]?.[0]).toContain('zod 4.2.0'); - warn.mockRestore(); - }); - - it('throws a clear error for zod 3 (vendor=zod, no ~standard.jsonSchema, no _zod)', () => { - // zod 3.24+ reports `~standard.vendor === 'zod'` but has no `_zod` internal marker. - const zod3ish = { _def: {}, '~standard': { version: 1, vendor: 'zod', validate: () => ({ value: {} }) } }; - expect(() => standardSchemaToJsonSchema(zod3ish as unknown as SchemaArg)).toThrow(/zod 3/); - expect(() => standardSchemaToJsonSchema(zod3ish as unknown as SchemaArg)).toThrow(/4\.2\.0/); - }); - - it('throws a clear error for non-zod libraries without ~standard.jsonSchema', () => { - const fake = { '~standard': { version: 1, vendor: 'mylib', validate: () => ({ value: {} }) } }; - expect(() => standardSchemaToJsonSchema(fake as unknown as SchemaArg)).toThrow(/mylib/); - expect(() => standardSchemaToJsonSchema(fake as unknown as SchemaArg)).toThrow(/fromJsonSchema/); - }); -}); diff --git a/packages/core/test/util/zodCompat.test.ts b/packages/core/test/util/zodCompat.test.ts deleted file mode 100644 index 5bdc229298..0000000000 --- a/packages/core/test/util/zodCompat.test.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { vi } from 'vitest'; -import * as z from 'zod/v4'; - -import { standardSchemaToJsonSchema } from '../../src/util/standardSchema'; -import { isZodRawShape, normalizeRawShapeSchema } from '../../src/util/zodCompat'; - -describe('isZodRawShape', () => { - test('treats empty object as a raw shape (matches v1)', () => { - expect(isZodRawShape({})).toBe(true); - }); - test('detects raw shape with zod fields', () => { - expect(isZodRawShape({ a: z.string() })).toBe(true); - }); - test('rejects a Standard Schema instance', () => { - expect(isZodRawShape(z.object({ a: z.string() }))).toBe(false); - }); - test('rejects a shape with non-Zod Standard Schema fields', () => { - const nonZod = { '~standard': { version: 1, vendor: 'arktype', validate: () => ({ value: 'x' }) } }; - expect(isZodRawShape({ a: nonZod })).toBe(false); - }); - test('rejects a shape with Zod v3 fields (only v4 is wrappable)', () => { - expect(isZodRawShape({ a: mockZodV3String() })).toBe(false); - }); - test('rejects non-plain objects with no own-enumerable properties', () => { - expect(isZodRawShape([])).toBe(false); - expect(isZodRawShape([z.string()])).toBe(false); - expect(isZodRawShape(new Date())).toBe(false); - expect(isZodRawShape(new Map())).toBe(false); - expect(isZodRawShape(/regex/)).toBe(false); - }); - test('accepts a null-prototype plain object', () => { - const o = Object.create(null); - o.a = z.string(); - expect(isZodRawShape(o)).toBe(true); - }); -}); - -// Minimal structural mock of a Zod v3 schema: has `_def.typeName` and -// `~standard.vendor === 'zod'` (zod >=3.24), but no `_zod`. -function mockZodV3String(): unknown { - return { - _def: { typeName: 'ZodString', checks: [], coerce: false }, - '~standard': { version: 1, vendor: 'zod', validate: (v: unknown) => ({ value: v }) }, - parse: (v: unknown) => v - }; -} - -describe('normalizeRawShapeSchema', () => { - test('wraps empty raw shape into z.object({})', () => { - const wrapped = normalizeRawShapeSchema({}); - expect(wrapped).toBeDefined(); - expect(standardSchemaToJsonSchema(wrapped!, 'input').type).toBe('object'); - }); - test('passes through an already-wrapped Standard Schema unchanged', () => { - const schema = z.object({ a: z.string() }); - expect(normalizeRawShapeSchema(schema)).toBe(schema); - }); - test('returns undefined for undefined input', () => { - expect(normalizeRawShapeSchema(undefined)).toBeUndefined(); - }); - test('throws TypeError for an invalid object that is neither raw shape nor Standard Schema', () => { - expect(() => normalizeRawShapeSchema({ a: 'not a zod schema' } as never)).toThrow(TypeError); - }); - test('passes through a Standard Schema without `~standard.jsonSchema` (per-vendor handling deferred to standardSchemaToJsonSchema)', () => { - const noJson = { '~standard': { version: 1, vendor: 'x', validate: () => ({ value: {} }) } }; - expect(normalizeRawShapeSchema(noJson as never)).toBe(noJson); - }); - test('passes through a zod 4.0-4.1 schema so standardSchemaToJsonSchema can apply its z.toJSONSchema fallback', () => { - const warn = vi.spyOn(console, 'warn').mockImplementation(() => {}); - const real = z.object({ a: z.string() }); - // Simulate zod 4.0-4.1: shadow `~standard` with `jsonSchema` removed, keep `_zod` intact. - const { jsonSchema: _drop, ...stdNoJson } = real['~standard'] as unknown as Record; - void _drop; - Object.defineProperty(real, '~standard', { value: { ...stdNoJson, vendor: 'zod' }, configurable: true }); - - const normalized = normalizeRawShapeSchema(real); - expect(normalized).toBe(real); - const json = standardSchemaToJsonSchema(normalized!, 'input'); - expect(json.type).toBe('object'); - expect((json.properties as Record)?.a).toBeDefined(); - warn.mockRestore(); - }); - test('throws actionable TypeError for a raw shape with Zod v3 fields', () => { - expect(() => normalizeRawShapeSchema({ a: mockZodV3String() } as never)).toThrow(/Zod v4 schemas.*Got a Zod v3 field schema/); - }); - test('throws the intended TypeError (not Object.values crash) for null input', () => { - expect(() => normalizeRawShapeSchema(null as never)).toThrow(/must be a Standard Schema/); - }); -}); diff --git a/packages/core/test/validators/validators.test.ts b/packages/core/test/validators/validators.test.ts deleted file mode 100644 index 7ffb4d16dc..0000000000 --- a/packages/core/test/validators/validators.test.ts +++ /dev/null @@ -1,625 +0,0 @@ -/** - * Tests all validator providers with various JSON Schema 2020-12 features - * Based on MCP specification for elicitation schemas: - * https://modelcontextprotocol.io/specification/draft/client/elicitation.md - */ - -import { readFileSync } from 'node:fs'; -import path from 'node:path'; - -import { vi } from 'vitest'; - -import { AjvJsonSchemaValidator } from '../../src/validators/ajvProvider'; -import { CfWorkerJsonSchemaValidator } from '../../src/validators/cfWorkerProvider'; -import type { JsonSchemaType } from '../../src/validators/types'; - -// Test with both AJV and CfWorker validators -// AJV validator will use default configuration with format validation enabled -const validators = [ - { name: 'AJV', provider: new AjvJsonSchemaValidator() }, - { name: 'CfWorker', provider: new CfWorkerJsonSchemaValidator() } -]; - -describe('JSON Schema Validators', () => { - describe.each(validators)('$name Validator', ({ provider }) => { - describe('String schemas', () => { - it('validates basic string', () => { - const schema: JsonSchemaType = { - type: 'string' - }; - const validator = provider.getValidator(schema); - - const validResult = validator('hello'); - expect(validResult.valid).toBe(true); - expect(validResult.data).toBe('hello'); - - const invalidResult = validator(123); - expect(invalidResult.valid).toBe(false); - expect(invalidResult.errorMessage).toBeDefined(); - }); - - it('validates string with title and description', () => { - const schema: JsonSchemaType = { - type: 'string', - title: 'Name', - description: "User's full name" - }; - const validator = provider.getValidator(schema); - - const result = validator('John Doe'); - expect(result.valid).toBe(true); - expect(result.data).toBe('John Doe'); - }); - - it('validates string with length constraints', () => { - const schema: JsonSchemaType = { - type: 'string', - minLength: 3, - maxLength: 10 - }; - const validator = provider.getValidator(schema); - - expect(validator('abc').valid).toBe(true); - expect(validator('abcdefghij').valid).toBe(true); - expect(validator('ab').valid).toBe(false); - expect(validator('abcdefghijk').valid).toBe(false); - }); - - it('validates email format', () => { - const schema: JsonSchemaType = { - type: 'string', - format: 'email' - }; - const validator = provider.getValidator(schema); - - expect(validator('user@example.com').valid).toBe(true); - expect(validator('invalid-email').valid).toBe(false); - }); - - it('validates URI format', () => { - const schema: JsonSchemaType = { - type: 'string', - format: 'uri' - }; - const validator = provider.getValidator(schema); - - expect(validator('https://example.com').valid).toBe(true); - expect(validator('not-a-uri').valid).toBe(false); - }); - - it('validates date-time format', () => { - const schema: JsonSchemaType = { - type: 'string', - format: 'date-time' - }; - const validator = provider.getValidator(schema); - - expect(validator('2025-10-17T12:00:00Z').valid).toBe(true); - expect(validator('not-a-date').valid).toBe(false); - }); - - it('validates string pattern', () => { - const schema: JsonSchemaType = { - type: 'string', - pattern: '^[A-Z]{3}$' - }; - const validator = provider.getValidator(schema); - - expect(validator('ABC').valid).toBe(true); - expect(validator('abc').valid).toBe(false); - expect(validator('ABCD').valid).toBe(false); - }); - }); - - describe('Number schemas', () => { - it('validates number type', () => { - const schema: JsonSchemaType = { - type: 'number' - }; - const validator = provider.getValidator(schema); - - expect(validator(42).valid).toBe(true); - expect(validator(3.14).valid).toBe(true); - expect(validator('42').valid).toBe(false); - }); - - it('validates integer type', () => { - const schema: JsonSchemaType = { - type: 'integer' - }; - const validator = provider.getValidator(schema); - - expect(validator(42).valid).toBe(true); - expect(validator(3.14).valid).toBe(false); - }); - - it('validates number range', () => { - const schema: JsonSchemaType = { - type: 'number', - minimum: 0, - maximum: 100 - }; - const validator = provider.getValidator(schema); - - expect(validator(0).valid).toBe(true); - expect(validator(50).valid).toBe(true); - expect(validator(100).valid).toBe(true); - expect(validator(-1).valid).toBe(false); - expect(validator(101).valid).toBe(false); - }); - }); - - describe('Boolean schemas', () => { - it('validates boolean type', () => { - const schema: JsonSchemaType = { - type: 'boolean' - }; - const validator = provider.getValidator(schema); - - expect(validator(true).valid).toBe(true); - expect(validator(false).valid).toBe(true); - expect(validator('true').valid).toBe(false); - expect(validator(1).valid).toBe(false); - }); - - it('validates boolean with default', () => { - const schema: JsonSchemaType = { - type: 'boolean', - default: false - }; - const validator = provider.getValidator(schema); - - expect(validator(true).valid).toBe(true); - expect(validator(false).valid).toBe(true); - }); - }); - - describe('Enum schemas', () => { - it('validates enum values', () => { - const schema: JsonSchemaType = { - enum: ['red', 'green', 'blue'] - }; - const validator = provider.getValidator(schema); - - expect(validator('red').valid).toBe(true); - expect(validator('green').valid).toBe(true); - expect(validator('blue').valid).toBe(true); - expect(validator('yellow').valid).toBe(false); - }); - - it('validates enum with mixed types', () => { - const schema: JsonSchemaType = { - enum: ['option1', 42, true, null] - }; - const validator = provider.getValidator(schema); - - expect(validator('option1').valid).toBe(true); - expect(validator(42).valid).toBe(true); - expect(validator(true).valid).toBe(true); - expect(validator(null).valid).toBe(true); - expect(validator('other').valid).toBe(false); - }); - }); - - describe('Object schemas', () => { - it('validates simple object', () => { - const schema: JsonSchemaType = { - type: 'object', - properties: { - name: { type: 'string' }, - age: { type: 'number' } - }, - required: ['name'] - }; - const validator = provider.getValidator(schema); - - expect(validator({ name: 'John', age: 30 }).valid).toBe(true); - expect(validator({ name: 'John' }).valid).toBe(true); - expect(validator({ age: 30 }).valid).toBe(false); - expect(validator({}).valid).toBe(false); - }); - - it('validates nested objects', () => { - const schema: JsonSchemaType = { - type: 'object', - properties: { - user: { - type: 'object', - properties: { - name: { type: 'string' }, - email: { type: 'string', format: 'email' } - }, - required: ['name'] - } - }, - required: ['user'] - }; - const validator = provider.getValidator(schema); - - expect( - validator({ - user: { name: 'John', email: 'john@example.com' } - }).valid - ).toBe(true); - - expect( - validator({ - user: { name: 'John' } - }).valid - ).toBe(true); - - expect( - validator({ - user: { email: 'john@example.com' } - }).valid - ).toBe(false); - }); - - it('validates object with additionalProperties: false', () => { - const schema: JsonSchemaType = { - type: 'object', - properties: { - name: { type: 'string' } - }, - additionalProperties: false - }; - const validator = provider.getValidator(schema); - - expect(validator({ name: 'John' }).valid).toBe(true); - expect(validator({ name: 'John', extra: 'field' }).valid).toBe(false); - }); - }); - - describe('Array schemas', () => { - it('validates array of strings', () => { - const schema: JsonSchemaType = { - type: 'array', - items: { type: 'string' } - }; - const validator = provider.getValidator(schema); - - expect(validator(['a', 'b', 'c']).valid).toBe(true); - expect(validator([]).valid).toBe(true); - expect(validator(['a', 1, 'c']).valid).toBe(false); - }); - - it('validates array length constraints', () => { - const schema: JsonSchemaType = { - type: 'array', - items: { type: 'number' }, - minItems: 1, - maxItems: 3 - }; - const validator = provider.getValidator(schema); - - expect(validator([1]).valid).toBe(true); - expect(validator([1, 2, 3]).valid).toBe(true); - expect(validator([]).valid).toBe(false); - expect(validator([1, 2, 3, 4]).valid).toBe(false); - }); - - it('validates array with unique items', () => { - const schema: JsonSchemaType = { - type: 'array', - items: { type: 'number' }, - uniqueItems: true - }; - const validator = provider.getValidator(schema); - - expect(validator([1, 2, 3]).valid).toBe(true); - expect(validator([1, 2, 2, 3]).valid).toBe(false); - }); - }); - - describe('JSON Schema 2020-12 features', () => { - it('validates schema with $schema field', () => { - const schema: JsonSchemaType = { - $schema: 'https://json-schema.org/draft/2020-12/schema', - type: 'string' - }; - const validator = provider.getValidator(schema); - - expect(validator('test').valid).toBe(true); - }); - - it('validates schema with $id field', () => { - const schema: JsonSchemaType = { - $id: 'https://example.com/schemas/test', - type: 'number' - }; - const validator = provider.getValidator(schema); - - expect(validator(42).valid).toBe(true); - }); - - it('validates with allOf', () => { - const schema: JsonSchemaType = { - allOf: [ - { type: 'object', properties: { name: { type: 'string' } } }, - { type: 'object', properties: { age: { type: 'number' } } } - ] - }; - const validator = provider.getValidator(schema); - - expect(validator({ name: 'John', age: 30 }).valid).toBe(true); - expect(validator({ name: 'John' }).valid).toBe(true); - expect(validator({ name: 123 }).valid).toBe(false); - }); - - it('validates with anyOf', () => { - const schema: JsonSchemaType = { - anyOf: [{ type: 'string' }, { type: 'number' }] - }; - const validator = provider.getValidator(schema); - - expect(validator('test').valid).toBe(true); - expect(validator(42).valid).toBe(true); - expect(validator(true).valid).toBe(false); - }); - - it('validates with oneOf', () => { - const schema: JsonSchemaType = { - oneOf: [ - { type: 'string', minLength: 5 }, - { type: 'string', maxLength: 3 } - ] - }; - const validator = provider.getValidator(schema); - - expect(validator('ab').valid).toBe(true); // Matches second only - expect(validator('hello').valid).toBe(true); // Matches first only - expect(validator('abcd').valid).toBe(false); // Matches neither - }); - - it('validates with not', () => { - const schema: JsonSchemaType = { - not: { type: 'null' } - }; - const validator = provider.getValidator(schema); - - expect(validator('test').valid).toBe(true); - expect(validator(42).valid).toBe(true); - expect(validator(null).valid).toBe(false); - }); - - it('validates with const', () => { - const schema: JsonSchemaType = { - const: 'specific-value' - }; - const validator = provider.getValidator(schema); - - expect(validator('specific-value').valid).toBe(true); - expect(validator('other-value').valid).toBe(false); - }); - }); - - describe('Complex real-world schemas', () => { - it('validates user registration form', () => { - const schema: JsonSchemaType = { - type: 'object', - properties: { - username: { - type: 'string', - minLength: 3, - maxLength: 20, - pattern: '^[a-zA-Z0-9_]+$' - }, - email: { - type: 'string', - format: 'email' - }, - age: { - type: 'integer', - minimum: 18, - maximum: 120 - }, - newsletter: { - type: 'boolean', - default: false - } - }, - required: ['username', 'email'] - }; - const validator = provider.getValidator(schema); - - expect( - validator({ - username: 'john_doe', - email: 'john@example.com', - age: 25, - newsletter: true - }).valid - ).toBe(true); - - expect( - validator({ - username: 'john_doe', - email: 'john@example.com' - }).valid - ).toBe(true); - - expect( - validator({ - username: 'ab', // Too short - email: 'john@example.com' - }).valid - ).toBe(false); - - expect( - validator({ - username: 'john_doe', - email: 'invalid-email' - }).valid - ).toBe(false); - }); - - it('validates API response with nested structure', () => { - const schema: JsonSchemaType = { - type: 'object', - properties: { - status: { - type: 'string', - enum: ['success', 'error', 'pending'] - }, - data: { - type: 'object', - properties: { - id: { type: 'string' }, - items: { - type: 'array', - items: { - type: 'object', - properties: { - name: { type: 'string' }, - quantity: { type: 'integer', minimum: 1 } - }, - required: ['name', 'quantity'] - } - } - }, - required: ['id', 'items'] - }, - timestamp: { - type: 'string', - format: 'date-time' - } - }, - required: ['status', 'data'] - }; - const validator = provider.getValidator(schema); - - expect( - validator({ - status: 'success', - data: { - id: '123', - items: [ - { name: 'Item 1', quantity: 5 }, - { name: 'Item 2', quantity: 3 } - ] - }, - timestamp: '2025-10-17T12:00:00Z' - }).valid - ).toBe(true); - - expect( - validator({ - status: 'invalid-status', - data: { id: '123', items: [] } - }).valid - ).toBe(false); - }); - }); - - describe('Error messages', () => { - it('provides helpful error message on validation failure', () => { - const schema: JsonSchemaType = { - type: 'object', - properties: { - name: { type: 'string' } - }, - required: ['name'] - }; - const validator = provider.getValidator(schema); - - const result = validator({}); - expect(result.valid).toBe(false); - expect(result.errorMessage).toBeDefined(); - expect(result.errorMessage).toBeTruthy(); - expect(typeof result.errorMessage).toBe('string'); - }); - }); - }); -}); - -describe('Missing dependencies', () => { - describe('AJV not installed but CfWorker is', () => { - beforeEach(() => { - vi.resetModules(); - }); - - afterEach(() => { - vi.doUnmock('ajv'); - vi.doUnmock('ajv-formats'); - }); - - it('should throw error when trying to import ajv-provider without ajv', async () => { - // Mock ajv as not installed - vi.doMock('ajv', () => { - throw new Error("Cannot find module 'ajv'"); - }); - - vi.doMock('ajv-formats', () => { - throw new Error("Cannot find module 'ajv-formats'"); - }); - - // Attempting to import ajv-provider should fail - await expect(import('../../src/validators/ajvProvider')).rejects.toThrow(); - }); - - it('should be able to import cfWorkerProvider when ajv is missing', async () => { - // Mock ajv as not installed - vi.doMock('ajv', () => { - throw new Error("Cannot find module 'ajv'"); - }); - - vi.doMock('ajv-formats', () => { - throw new Error("Cannot find module 'ajv-formats'"); - }); - - // But cfWorkerProvider should import successfully - const cfworkerModule = await import('../../src/validators/cfWorkerProvider'); - expect(cfworkerModule.CfWorkerJsonSchemaValidator).toBeDefined(); - - // And should work correctly - const validator = new cfworkerModule.CfWorkerJsonSchemaValidator(); - const schema: JsonSchemaType = { type: 'string' }; - const validatorFn = validator.getValidator(schema); - expect(validatorFn('test').valid).toBe(true); - }); - }); - - describe('CfWorker not installed but AJV is', () => { - beforeEach(() => { - vi.resetModules(); - }); - - afterEach(() => { - vi.doUnmock('@cfworker/json-schema'); - }); - - it('should throw error when trying to import cfWorkerProvider without @cfworker/json-schema', async () => { - // Mock @cfworker/json-schema as not installed - vi.doMock('@cfworker/json-schema', () => { - throw new Error("Cannot find module '@cfworker/json-schema'"); - }); - - // Attempting to import cfWorkerProvider should fail - await expect(import('../../src/validators/cfWorkerProvider')).rejects.toThrow(); - }); - - it('should be able to import ajv-provider when @cfworker/json-schema is missing', async () => { - // Mock @cfworker/json-schema as not installed - vi.doMock('@cfworker/json-schema', () => { - throw new Error("Cannot find module '@cfworker/json-schema'"); - }); - - // But ajv-provider should import successfully - const ajvModule = await import('../../src/validators/ajvProvider'); - expect(ajvModule.AjvJsonSchemaValidator).toBeDefined(); - - // And should work correctly - const validator = new ajvModule.AjvJsonSchemaValidator(); - const schema: JsonSchemaType = { type: 'string' }; - const validatorFn = validator.getValidator(schema); - expect(validatorFn('test').valid).toBe(true); - }); - - it('should document that @cfworker/json-schema is required', () => { - const cfworkerProviderPath = path.join(__dirname, '../../src/validators/cfWorkerProvider.ts'); - const content = readFileSync(cfworkerProviderPath, 'utf8'); - - expect(content).toContain('@cfworker/json-schema'); - }); - }); -}); diff --git a/packages/core/tsconfig.json b/packages/core/tsconfig.json index a6838303e4..e150389b59 100644 --- a/packages/core/tsconfig.json +++ b/packages/core/tsconfig.json @@ -5,8 +5,8 @@ "compilerOptions": { "paths": { "*": ["./*"], - "@modelcontextprotocol/eslint-config": ["./node_modules/@modelcontextprotocol/eslint-config/tsconfig.json"], - "@modelcontextprotocol/vitest-config": ["./node_modules/@modelcontextprotocol/vitest-config/tsconfig.json"] + "@modelcontextprotocol/core-internal/schemas": ["./node_modules/@modelcontextprotocol/core-internal/src/types/schemas.ts"], + "@modelcontextprotocol/core-internal/auth": ["./node_modules/@modelcontextprotocol/core-internal/src/shared/auth.ts"] } } } diff --git a/packages/sdk-shared/tsdown.config.ts b/packages/core/tsdown.config.ts similarity index 59% rename from packages/sdk-shared/tsdown.config.ts rename to packages/core/tsdown.config.ts index 0d55609f6b..a0e3e12e76 100644 --- a/packages/sdk-shared/tsdown.config.ts +++ b/packages/core/tsdown.config.ts @@ -1,10 +1,10 @@ import { defineConfig } from 'tsdown'; -// sdk-shared re-exports ONLY the spec + OAuth Zod schemas from @modelcontextprotocol/core (private, +// core re-exports ONLY the spec + OAuth Zod schemas from @modelcontextprotocol/core-internal (private, // unpublished). Two BUILD-ONLY subpath aliases (not real core exports) point at core's two schema // modules, kept as separate sources: -// @modelcontextprotocol/core/schemas → core/src/types/schemas.ts (MCP spec schemas) -// @modelcontextprotocol/core/auth → core/src/shared/auth.ts (OAuth/OpenID schemas) +// @modelcontextprotocol/core-internal/schemas → core/src/types/schemas.ts (MCP spec schemas) +// @modelcontextprotocol/core-internal/auth → core/src/shared/auth.ts (OAuth/OpenID schemas) // Aliasing to these modules rather than core's barrel keeps the bundled graph to just the schemas + // the constants they use — never Protocol, transports, stdio, or the ajv/cfWorker validators. Both // modules import only `zod/v4`, so the graph stays runtime-neutral; `platform: 'neutral'` makes a @@ -23,10 +23,10 @@ export default defineConfig({ compilerOptions: { baseUrl: '.', paths: { - '@modelcontextprotocol/core/schemas': ['../core/src/types/schemas.ts'], - '@modelcontextprotocol/core/auth': ['../core/src/shared/auth.ts'] + '@modelcontextprotocol/core-internal/schemas': ['../core-internal/src/types/schemas.ts'], + '@modelcontextprotocol/core-internal/auth': ['../core-internal/src/shared/auth.ts'] } } }, - noExternal: ['@modelcontextprotocol/core/schemas', '@modelcontextprotocol/core/auth'] + noExternal: ['@modelcontextprotocol/core-internal/schemas', '@modelcontextprotocol/core-internal/auth'] }); diff --git a/packages/sdk-shared/typedoc.json b/packages/core/typedoc.json similarity index 100% rename from packages/sdk-shared/typedoc.json rename to packages/core/typedoc.json diff --git a/packages/middleware/express/tsconfig.json b/packages/middleware/express/tsconfig.json index 0292cb0c22..11330e7a4a 100644 --- a/packages/middleware/express/tsconfig.json +++ b/packages/middleware/express/tsconfig.json @@ -7,11 +7,11 @@ "*": ["./*"], "@modelcontextprotocol/server": ["./node_modules/@modelcontextprotocol/server/src/index.ts"], "@modelcontextprotocol/server/_shims": ["./node_modules/@modelcontextprotocol/server/src/shimsNode.ts"], - "@modelcontextprotocol/core": [ - "./node_modules/@modelcontextprotocol/server/node_modules/@modelcontextprotocol/core/src/index.ts" + "@modelcontextprotocol/core-internal": [ + "./node_modules/@modelcontextprotocol/server/node_modules/@modelcontextprotocol/core-internal/src/index.ts" ], - "@modelcontextprotocol/core/public": [ - "./node_modules/@modelcontextprotocol/server/node_modules/@modelcontextprotocol/core/src/exports/public/index.ts" + "@modelcontextprotocol/core-internal/public": [ + "./node_modules/@modelcontextprotocol/server/node_modules/@modelcontextprotocol/core-internal/src/exports/public/index.ts" ] } } diff --git a/packages/middleware/fastify/tsconfig.json b/packages/middleware/fastify/tsconfig.json index c92435851b..62f257e788 100644 --- a/packages/middleware/fastify/tsconfig.json +++ b/packages/middleware/fastify/tsconfig.json @@ -7,8 +7,8 @@ "*": ["./*"], "@modelcontextprotocol/server": ["./node_modules/@modelcontextprotocol/server/src/index.ts"], "@modelcontextprotocol/server/_shims": ["./node_modules/@modelcontextprotocol/server/src/shimsNode.ts"], - "@modelcontextprotocol/core": [ - "./node_modules/@modelcontextprotocol/server/node_modules/@modelcontextprotocol/core/src/index.ts" + "@modelcontextprotocol/core-internal": [ + "./node_modules/@modelcontextprotocol/server/node_modules/@modelcontextprotocol/core-internal/src/index.ts" ] } } diff --git a/packages/middleware/hono/tsconfig.json b/packages/middleware/hono/tsconfig.json index 0292cb0c22..11330e7a4a 100644 --- a/packages/middleware/hono/tsconfig.json +++ b/packages/middleware/hono/tsconfig.json @@ -7,11 +7,11 @@ "*": ["./*"], "@modelcontextprotocol/server": ["./node_modules/@modelcontextprotocol/server/src/index.ts"], "@modelcontextprotocol/server/_shims": ["./node_modules/@modelcontextprotocol/server/src/shimsNode.ts"], - "@modelcontextprotocol/core": [ - "./node_modules/@modelcontextprotocol/server/node_modules/@modelcontextprotocol/core/src/index.ts" + "@modelcontextprotocol/core-internal": [ + "./node_modules/@modelcontextprotocol/server/node_modules/@modelcontextprotocol/core-internal/src/index.ts" ], - "@modelcontextprotocol/core/public": [ - "./node_modules/@modelcontextprotocol/server/node_modules/@modelcontextprotocol/core/src/exports/public/index.ts" + "@modelcontextprotocol/core-internal/public": [ + "./node_modules/@modelcontextprotocol/server/node_modules/@modelcontextprotocol/core-internal/src/exports/public/index.ts" ] } } diff --git a/packages/middleware/node/eslint.config.mjs b/packages/middleware/node/eslint.config.mjs index 4f034f2235..dd9e88588a 100644 --- a/packages/middleware/node/eslint.config.mjs +++ b/packages/middleware/node/eslint.config.mjs @@ -6,7 +6,7 @@ export default [ ...baseConfig, { settings: { - 'import/internal-regex': '^@modelcontextprotocol/core' + 'import/internal-regex': '^@modelcontextprotocol/core-internal' } } ]; diff --git a/packages/middleware/node/package.json b/packages/middleware/node/package.json index 9d6ed525d9..7e610d3abb 100644 --- a/packages/middleware/node/package.json +++ b/packages/middleware/node/package.json @@ -62,7 +62,7 @@ }, "devDependencies": { "@modelcontextprotocol/server": "workspace:^", - "@modelcontextprotocol/core": "workspace:^", + "@modelcontextprotocol/core-internal": "workspace:^", "@modelcontextprotocol/eslint-config": "workspace:^", "@modelcontextprotocol/test-helpers": "workspace:^", "@modelcontextprotocol/tsconfig": "workspace:^", diff --git a/packages/middleware/node/test/streamableHttp.test.ts b/packages/middleware/node/test/streamableHttp.test.ts index e56166a279..140717c6bb 100644 --- a/packages/middleware/node/test/streamableHttp.test.ts +++ b/packages/middleware/node/test/streamableHttp.test.ts @@ -11,7 +11,7 @@ import type { JSONRPCMessage, JSONRPCResultResponse, RequestId -} from '@modelcontextprotocol/core'; +} from '@modelcontextprotocol/core-internal'; import type { EventId, EventStore, StreamId } from '@modelcontextprotocol/server'; import { McpServer } from '@modelcontextprotocol/server'; import { listenOnRandomPort } from '@modelcontextprotocol/test-helpers'; diff --git a/packages/middleware/node/tsconfig.json b/packages/middleware/node/tsconfig.json index 0985895356..7cd442d0d4 100644 --- a/packages/middleware/node/tsconfig.json +++ b/packages/middleware/node/tsconfig.json @@ -7,8 +7,10 @@ "*": ["./*"], "@modelcontextprotocol/server": ["./node_modules/@modelcontextprotocol/server/src/index.ts"], "@modelcontextprotocol/server/_shims": ["./node_modules/@modelcontextprotocol/server/src/shimsNode.ts"], - "@modelcontextprotocol/core": ["./node_modules/@modelcontextprotocol/core/src/index.ts"], - "@modelcontextprotocol/core/public": ["./node_modules/@modelcontextprotocol/core/src/exports/public/index.ts"], + "@modelcontextprotocol/core-internal": ["./node_modules/@modelcontextprotocol/core-internal/src/index.ts"], + "@modelcontextprotocol/core-internal/public": [ + "./node_modules/@modelcontextprotocol/core-internal/src/exports/public/index.ts" + ], "@modelcontextprotocol/test-helpers": ["./node_modules/@modelcontextprotocol/test-helpers/src/index.ts"] } } diff --git a/packages/sdk-shared/eslint.config.mjs b/packages/sdk-shared/eslint.config.mjs deleted file mode 100644 index 4f034f2235..0000000000 --- a/packages/sdk-shared/eslint.config.mjs +++ /dev/null @@ -1,12 +0,0 @@ -// @ts-check - -import baseConfig from '@modelcontextprotocol/eslint-config'; - -export default [ - ...baseConfig, - { - settings: { - 'import/internal-regex': '^@modelcontextprotocol/core' - } - } -]; diff --git a/packages/sdk-shared/package.json b/packages/sdk-shared/package.json deleted file mode 100644 index 4e54407c72..0000000000 --- a/packages/sdk-shared/package.json +++ /dev/null @@ -1,63 +0,0 @@ -{ - "name": "@modelcontextprotocol/sdk-shared", - "version": "2.0.0-alpha.0", - "description": "Model Context Protocol implementation for TypeScript - shared spec Zod schemas", - "license": "MIT", - "author": "Anthropic, PBC (https://anthropic.com)", - "homepage": "https://modelcontextprotocol.io", - "bugs": "https://github.com/modelcontextprotocol/typescript-sdk/issues", - "type": "module", - "repository": { - "type": "git", - "url": "git+https://github.com/modelcontextprotocol/typescript-sdk.git" - }, - "engines": { - "node": ">=20" - }, - "keywords": [ - "modelcontextprotocol", - "mcp", - "schemas", - "zod" - ], - "exports": { - ".": { - "types": "./dist/index.d.ts", - "import": "./dist/index.js" - } - }, - "types": "./dist/index.d.ts", - "files": [ - "dist" - ], - "scripts": { - "typecheck": "tsgo -p tsconfig.json --noEmit", - "build": "tsdown", - "build:watch": "tsdown --watch", - "prepack": "pnpm run build", - "lint": "eslint src/ && prettier --ignore-path ../../.prettierignore --check .", - "lint:fix": "eslint src/ --fix && prettier --ignore-path ../../.prettierignore --write .", - "check": "pnpm run typecheck && pnpm run lint", - "test": "vitest run", - "test:watch": "vitest" - }, - "dependencies": { - "zod": "catalog:runtimeShared" - }, - "devDependencies": { - "@eslint/js": "catalog:devTools", - "@modelcontextprotocol/core": "workspace:^", - "@modelcontextprotocol/eslint-config": "workspace:^", - "@modelcontextprotocol/tsconfig": "workspace:^", - "@modelcontextprotocol/vitest-config": "workspace:^", - "@typescript/native-preview": "catalog:devTools", - "eslint": "catalog:devTools", - "eslint-config-prettier": "catalog:devTools", - "eslint-plugin-n": "catalog:devTools", - "prettier": "catalog:devTools", - "tsdown": "catalog:devTools", - "typescript": "catalog:devTools", - "typescript-eslint": "catalog:devTools", - "vitest": "catalog:devTools" - } -} diff --git a/packages/sdk-shared/src/index.ts b/packages/sdk-shared/src/index.ts deleted file mode 100644 index ed0b62fbe9..0000000000 --- a/packages/sdk-shared/src/index.ts +++ /dev/null @@ -1,198 +0,0 @@ -// @modelcontextprotocol/sdk-shared -// -// Canonical public home for the Model Context Protocol specification + OAuth/OpenID Zod schemas. -// -// These are the exact schema constants the SDK validates against internally (defined in the -// private @modelcontextprotocol/core package). This package bundles core and re-exports ONLY the -// `*Schema` Zod values, so consumers can validate protocol/OAuth payloads directly — e.g. -// `CallToolResultSchema.parse(value)` / `.safeParse(value)` — without depending on core's -// internal barrel. -// -// Scope: Zod schemas ONLY. The corresponding spec TypeScript types, error classes, enums, and -// type guards are part of the public API of @modelcontextprotocol/server and /client. -// -// Two groups, kept separate to mirror core's own spec-vs-auth split, each bundled from a build-only -// subpath alias of core (tsconfig.json + tsdown.config.ts): -// - SPEC schemas, from @modelcontextprotocol/core/schemas (core/src/types/schemas.ts): every -// `export const *Schema` EXCEPT internal helpers with no public spec type (e.g. -// BaseRequestParamsSchema). Mirrors core's SPEC_SCHEMA_KEYS allowlist. -// - OAUTH/OPENID schemas, from @modelcontextprotocol/core/auth (core/src/shared/auth.ts). -// The sdkSharedSchemas test asserts both groups stay in sync with their core source modules. -export { - AnnotationsSchema, - AudioContentSchema, - BaseMetadataSchema, - BlobResourceContentsSchema, - BooleanSchemaSchema, - CallToolRequestParamsSchema, - CallToolRequestSchema, - CallToolResultSchema, - CancelledNotificationParamsSchema, - CancelledNotificationSchema, - CancelTaskRequestSchema, - CancelTaskResultSchema, - ClientCapabilitiesSchema, - ClientNotificationSchema, - ClientRequestSchema, - ClientResultSchema, - CompatibilityCallToolResultSchema, - CompleteRequestParamsSchema, - CompleteRequestSchema, - CompleteResultSchema, - ContentBlockSchema, - CreateMessageRequestParamsSchema, - CreateMessageRequestSchema, - CreateMessageResultSchema, - CreateMessageResultWithToolsSchema, - CreateTaskResultSchema, - CursorSchema, - DiscoverRequestSchema, - DiscoverResultSchema, - ElicitationCompleteNotificationParamsSchema, - ElicitationCompleteNotificationSchema, - ElicitRequestFormParamsSchema, - ElicitRequestParamsSchema, - ElicitRequestSchema, - ElicitRequestURLParamsSchema, - ElicitResultSchema, - EmbeddedResourceSchema, - EmptyResultSchema, - EnumSchemaSchema, - GetPromptRequestParamsSchema, - GetPromptRequestSchema, - GetPromptResultSchema, - GetTaskPayloadRequestSchema, - GetTaskPayloadResultSchema, - GetTaskRequestSchema, - GetTaskResultSchema, - IconSchema, - IconsSchema, - ImageContentSchema, - ImplementationSchema, - InitializedNotificationSchema, - InitializeRequestParamsSchema, - InitializeRequestSchema, - InitializeResultSchema, - JSONArraySchema, - JSONObjectSchema, - JSONRPCErrorResponseSchema, - JSONRPCMessageSchema, - JSONRPCNotificationSchema, - JSONRPCRequestSchema, - JSONRPCResponseSchema, - JSONRPCResultResponseSchema, - JSONValueSchema, - LegacyTitledEnumSchemaSchema, - ListPromptsRequestSchema, - ListPromptsResultSchema, - ListResourcesRequestSchema, - ListResourcesResultSchema, - ListResourceTemplatesRequestSchema, - ListResourceTemplatesResultSchema, - ListRootsRequestSchema, - ListRootsResultSchema, - ListTasksRequestSchema, - ListTasksResultSchema, - ListToolsRequestSchema, - ListToolsResultSchema, - LoggingLevelSchema, - LoggingMessageNotificationParamsSchema, - LoggingMessageNotificationSchema, - ModelHintSchema, - ModelPreferencesSchema, - MultiSelectEnumSchemaSchema, - NotificationSchema, - NumberSchemaSchema, - PaginatedRequestParamsSchema, - PaginatedRequestSchema, - PaginatedResultSchema, - PingRequestSchema, - PrimitiveSchemaDefinitionSchema, - ProgressNotificationParamsSchema, - ProgressNotificationSchema, - ProgressSchema, - ProgressTokenSchema, - PromptArgumentSchema, - PromptListChangedNotificationSchema, - PromptMessageSchema, - PromptReferenceSchema, - PromptSchema, - ReadResourceRequestParamsSchema, - ReadResourceRequestSchema, - ReadResourceResultSchema, - RelatedTaskMetadataSchema, - RequestIdSchema, - RequestMetaEnvelopeSchema, - RequestMetaSchema, - RequestSchema, - ResourceContentsSchema, - ResourceLinkSchema, - ResourceListChangedNotificationSchema, - ResourceRequestParamsSchema, - ResourceSchema, - ResourceTemplateReferenceSchema, - ResourceTemplateSchema, - ResourceUpdatedNotificationParamsSchema, - ResourceUpdatedNotificationSchema, - ResultSchema, - RoleSchema, - RootSchema, - RootsListChangedNotificationSchema, - SamplingContentSchema, - SamplingMessageContentBlockSchema, - SamplingMessageSchema, - ServerCapabilitiesSchema, - ServerNotificationSchema, - ServerRequestSchema, - ServerResultSchema, - SetLevelRequestParamsSchema, - SetLevelRequestSchema, - SingleSelectEnumSchemaSchema, - StringSchemaSchema, - SubscribeRequestParamsSchema, - SubscribeRequestSchema, - TaskAugmentedRequestParamsSchema, - TaskCreationParamsSchema, - TaskMetadataSchema, - TaskSchema, - TaskStatusNotificationParamsSchema, - TaskStatusNotificationSchema, - TaskStatusSchema, - TextContentSchema, - TextResourceContentsSchema, - TitledMultiSelectEnumSchemaSchema, - TitledSingleSelectEnumSchemaSchema, - ToolAnnotationsSchema, - ToolChoiceSchema, - ToolExecutionSchema, - ToolListChangedNotificationSchema, - ToolResultContentSchema, - ToolSchema, - ToolUseContentSchema, - UnsubscribeRequestParamsSchema, - UnsubscribeRequestSchema, - UntitledMultiSelectEnumSchemaSchema, - UntitledSingleSelectEnumSchemaSchema -} from '@modelcontextprotocol/core/schemas'; - -// Auth schemas (OAuth / OpenID / IdJag) — kept as a SEPARATE group from the MCP spec schemas above, -// mirroring core's own spec-vs-auth split (these live in core/src/shared/auth.ts, not types/schemas.ts, -// and are registered as `authSchemas` in core's specTypeSchema.ts). This group is EXACTLY core's -// `authSchemas` set — every auth schema that has a public spec type (so `isSpecType.OAuthTokens`, -// `isSpecType.IdJagTokenExchangeResponse`, etc. exist). The typeless internal URL field-validators -// (SafeUrlSchema, OptionalSafeUrlSchema) are not auth schemas and stay out. The sdkSharedSchemas test -// asserts this group stays in sync with core's `authSchemas`. -export { - IdJagTokenExchangeResponseSchema, - OAuthClientInformationFullSchema, - OAuthClientInformationSchema, - OAuthClientMetadataSchema, - OAuthClientRegistrationErrorSchema, - OAuthErrorResponseSchema, - OAuthMetadataSchema, - OAuthProtectedResourceMetadataSchema, - OAuthTokenRevocationRequestSchema, - OAuthTokensSchema, - OpenIdProviderDiscoveryMetadataSchema, - OpenIdProviderMetadataSchema -} from '@modelcontextprotocol/core/auth'; diff --git a/packages/sdk-shared/tsconfig.json b/packages/sdk-shared/tsconfig.json deleted file mode 100644 index f75f697009..0000000000 --- a/packages/sdk-shared/tsconfig.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "extends": "@modelcontextprotocol/tsconfig", - "include": ["./"], - "exclude": ["node_modules", "dist"], - "compilerOptions": { - "paths": { - "*": ["./*"], - "@modelcontextprotocol/core/schemas": ["./node_modules/@modelcontextprotocol/core/src/types/schemas.ts"], - "@modelcontextprotocol/core/auth": ["./node_modules/@modelcontextprotocol/core/src/shared/auth.ts"] - } - } -} diff --git a/packages/sdk-shared/vitest.config.js b/packages/sdk-shared/vitest.config.js deleted file mode 100644 index 496fca3200..0000000000 --- a/packages/sdk-shared/vitest.config.js +++ /dev/null @@ -1,3 +0,0 @@ -import baseConfig from '@modelcontextprotocol/vitest-config'; - -export default baseConfig; diff --git a/packages/server-legacy/eslint.config.mjs b/packages/server-legacy/eslint.config.mjs index 4f034f2235..dd9e88588a 100644 --- a/packages/server-legacy/eslint.config.mjs +++ b/packages/server-legacy/eslint.config.mjs @@ -6,7 +6,7 @@ export default [ ...baseConfig, { settings: { - 'import/internal-regex': '^@modelcontextprotocol/core' + 'import/internal-regex': '^@modelcontextprotocol/core-internal' } } ]; diff --git a/packages/server-legacy/package.json b/packages/server-legacy/package.json index c320fe349d..b23858abfd 100644 --- a/packages/server-legacy/package.json +++ b/packages/server-legacy/package.json @@ -80,7 +80,7 @@ } }, "devDependencies": { - "@modelcontextprotocol/core": "workspace:^", + "@modelcontextprotocol/core-internal": "workspace:^", "@modelcontextprotocol/tsconfig": "workspace:^", "@modelcontextprotocol/eslint-config": "workspace:^", "@modelcontextprotocol/vitest-config": "workspace:^", diff --git a/packages/server-legacy/src/auth/clients.ts b/packages/server-legacy/src/auth/clients.ts index f6aca1be92..0c9412ca18 100644 --- a/packages/server-legacy/src/auth/clients.ts +++ b/packages/server-legacy/src/auth/clients.ts @@ -1,4 +1,4 @@ -import type { OAuthClientInformationFull } from '@modelcontextprotocol/core'; +import type { OAuthClientInformationFull } from '@modelcontextprotocol/core-internal'; /** * Stores information about registered OAuth clients for this server. diff --git a/packages/server-legacy/src/auth/errors.ts b/packages/server-legacy/src/auth/errors.ts index eac277779c..236ed22d1b 100644 --- a/packages/server-legacy/src/auth/errors.ts +++ b/packages/server-legacy/src/auth/errors.ts @@ -1,4 +1,4 @@ -import type { OAuthErrorResponse } from '@modelcontextprotocol/core'; +import type { OAuthErrorResponse } from '@modelcontextprotocol/core-internal'; /** * Base class for all OAuth errors diff --git a/packages/server-legacy/src/auth/handlers/metadata.ts b/packages/server-legacy/src/auth/handlers/metadata.ts index 23230e3d3f..e75b493320 100644 --- a/packages/server-legacy/src/auth/handlers/metadata.ts +++ b/packages/server-legacy/src/auth/handlers/metadata.ts @@ -1,4 +1,4 @@ -import type { OAuthMetadata, OAuthProtectedResourceMetadata } from '@modelcontextprotocol/core'; +import type { OAuthMetadata, OAuthProtectedResourceMetadata } from '@modelcontextprotocol/core-internal'; import cors from 'cors'; import type { RequestHandler } from 'express'; import express from 'express'; diff --git a/packages/server-legacy/src/auth/handlers/register.ts b/packages/server-legacy/src/auth/handlers/register.ts index 1cc19e96e9..d180ff35f7 100644 --- a/packages/server-legacy/src/auth/handlers/register.ts +++ b/packages/server-legacy/src/auth/handlers/register.ts @@ -1,7 +1,7 @@ import crypto from 'node:crypto'; -import type { OAuthClientInformationFull } from '@modelcontextprotocol/core'; -import { OAuthClientMetadataSchema } from '@modelcontextprotocol/core'; +import type { OAuthClientInformationFull } from '@modelcontextprotocol/core-internal'; +import { OAuthClientMetadataSchema } from '@modelcontextprotocol/core-internal'; import cors from 'cors'; import type { RequestHandler } from 'express'; import express from 'express'; diff --git a/packages/server-legacy/src/auth/handlers/revoke.ts b/packages/server-legacy/src/auth/handlers/revoke.ts index 0d39a80ece..218d89beba 100644 --- a/packages/server-legacy/src/auth/handlers/revoke.ts +++ b/packages/server-legacy/src/auth/handlers/revoke.ts @@ -1,4 +1,4 @@ -import { OAuthTokenRevocationRequestSchema } from '@modelcontextprotocol/core'; +import { OAuthTokenRevocationRequestSchema } from '@modelcontextprotocol/core-internal'; import cors from 'cors'; import type { RequestHandler } from 'express'; import express from 'express'; diff --git a/packages/server-legacy/src/auth/middleware/clientAuth.ts b/packages/server-legacy/src/auth/middleware/clientAuth.ts index 611d797d1a..ad9d931253 100644 --- a/packages/server-legacy/src/auth/middleware/clientAuth.ts +++ b/packages/server-legacy/src/auth/middleware/clientAuth.ts @@ -1,4 +1,4 @@ -import type { OAuthClientInformationFull } from '@modelcontextprotocol/core'; +import type { OAuthClientInformationFull } from '@modelcontextprotocol/core-internal'; import type { RequestHandler } from 'express'; import * as z from 'zod/v4'; diff --git a/packages/server-legacy/src/auth/provider.ts b/packages/server-legacy/src/auth/provider.ts index 0884997272..47e8836a3d 100644 --- a/packages/server-legacy/src/auth/provider.ts +++ b/packages/server-legacy/src/auth/provider.ts @@ -1,4 +1,4 @@ -import type { OAuthClientInformationFull, OAuthTokenRevocationRequest, OAuthTokens } from '@modelcontextprotocol/core'; +import type { OAuthClientInformationFull, OAuthTokenRevocationRequest, OAuthTokens } from '@modelcontextprotocol/core-internal'; import type { Response } from 'express'; import type { OAuthRegisteredClientsStore } from './clients'; diff --git a/packages/server-legacy/src/auth/providers/proxyProvider.ts b/packages/server-legacy/src/auth/providers/proxyProvider.ts index 622b576b0e..db85c34da6 100644 --- a/packages/server-legacy/src/auth/providers/proxyProvider.ts +++ b/packages/server-legacy/src/auth/providers/proxyProvider.ts @@ -1,5 +1,5 @@ -import type { FetchLike, OAuthClientInformationFull, OAuthTokenRevocationRequest, OAuthTokens } from '@modelcontextprotocol/core'; -import { OAuthClientInformationFullSchema, OAuthTokensSchema } from '@modelcontextprotocol/core'; +import type { FetchLike, OAuthClientInformationFull, OAuthTokenRevocationRequest, OAuthTokens } from '@modelcontextprotocol/core-internal'; +import { OAuthClientInformationFullSchema, OAuthTokensSchema } from '@modelcontextprotocol/core-internal'; import type { Response } from 'express'; import type { OAuthRegisteredClientsStore } from '../clients'; diff --git a/packages/server-legacy/src/auth/router.ts b/packages/server-legacy/src/auth/router.ts index 71cd3d54ec..6ffa1e36fc 100644 --- a/packages/server-legacy/src/auth/router.ts +++ b/packages/server-legacy/src/auth/router.ts @@ -1,4 +1,4 @@ -import type { OAuthMetadata, OAuthProtectedResourceMetadata } from '@modelcontextprotocol/core'; +import type { OAuthMetadata, OAuthProtectedResourceMetadata } from '@modelcontextprotocol/core-internal'; import type { RequestHandler } from 'express'; import express from 'express'; diff --git a/packages/server-legacy/src/auth/types.ts b/packages/server-legacy/src/auth/types.ts index 3ccfcdc344..1168c8be9b 100644 --- a/packages/server-legacy/src/auth/types.ts +++ b/packages/server-legacy/src/auth/types.ts @@ -1 +1 @@ -export type { AuthInfo } from '@modelcontextprotocol/core'; +export type { AuthInfo } from '@modelcontextprotocol/core-internal'; diff --git a/packages/server-legacy/src/sse/sse.ts b/packages/server-legacy/src/sse/sse.ts index 7c9a4eab6f..6add66f9f9 100644 --- a/packages/server-legacy/src/sse/sse.ts +++ b/packages/server-legacy/src/sse/sse.ts @@ -2,8 +2,8 @@ import { randomUUID } from 'node:crypto'; import type { IncomingMessage, ServerResponse } from 'node:http'; import { TLSSocket } from 'node:tls'; -import type { AuthInfo, JSONRPCMessage, MessageExtraInfo, Transport, TransportSendOptions } from '@modelcontextprotocol/core'; -import { JSONRPCMessageSchema } from '@modelcontextprotocol/core'; +import type { AuthInfo, JSONRPCMessage, MessageExtraInfo, Transport, TransportSendOptions } from '@modelcontextprotocol/core-internal'; +import { JSONRPCMessageSchema } from '@modelcontextprotocol/core-internal'; import contentType from 'content-type'; import getRawBody from 'raw-body'; diff --git a/packages/server-legacy/test/auth/handlers/authorize.test.ts b/packages/server-legacy/test/auth/handlers/authorize.test.ts index bd11269349..dc0e9bbf0f 100644 --- a/packages/server-legacy/test/auth/handlers/authorize.test.ts +++ b/packages/server-legacy/test/auth/handlers/authorize.test.ts @@ -1,7 +1,7 @@ import { authorizationHandler, AuthorizationHandlerOptions, redirectUriMatches } from '../../../src/auth/handlers/authorize'; import { OAuthServerProvider, AuthorizationParams } from '../../../src/auth/provider'; import { OAuthRegisteredClientsStore } from '../../../src/auth/clients'; -import { OAuthClientInformationFull, OAuthTokens } from '@modelcontextprotocol/core'; +import { OAuthClientInformationFull, OAuthTokens } from '@modelcontextprotocol/core-internal'; import express, { Response } from 'express'; import supertest from 'supertest'; import { AuthInfo } from '../../../src/auth/types'; diff --git a/packages/server-legacy/test/auth/handlers/metadata.test.ts b/packages/server-legacy/test/auth/handlers/metadata.test.ts index bf77b3badb..46f26b98d7 100644 --- a/packages/server-legacy/test/auth/handlers/metadata.test.ts +++ b/packages/server-legacy/test/auth/handlers/metadata.test.ts @@ -1,5 +1,5 @@ import { metadataHandler } from '../../../src/auth/handlers/metadata'; -import { OAuthMetadata } from '@modelcontextprotocol/core'; +import { OAuthMetadata } from '@modelcontextprotocol/core-internal'; import express from 'express'; import supertest from 'supertest'; diff --git a/packages/server-legacy/test/auth/handlers/register.test.ts b/packages/server-legacy/test/auth/handlers/register.test.ts index bcdafdc264..295515a89a 100644 --- a/packages/server-legacy/test/auth/handlers/register.test.ts +++ b/packages/server-legacy/test/auth/handlers/register.test.ts @@ -1,6 +1,6 @@ import { clientRegistrationHandler, ClientRegistrationHandlerOptions } from '../../../src/auth/handlers/register'; import { OAuthRegisteredClientsStore } from '../../../src/auth/clients'; -import { OAuthClientInformationFull, OAuthClientMetadata } from '@modelcontextprotocol/core'; +import { OAuthClientInformationFull, OAuthClientMetadata } from '@modelcontextprotocol/core-internal'; import express from 'express'; import supertest from 'supertest'; import { MockInstance } from 'vitest'; diff --git a/packages/server-legacy/test/auth/handlers/revoke.test.ts b/packages/server-legacy/test/auth/handlers/revoke.test.ts index ba3343fc74..eb4caf52f7 100644 --- a/packages/server-legacy/test/auth/handlers/revoke.test.ts +++ b/packages/server-legacy/test/auth/handlers/revoke.test.ts @@ -1,7 +1,7 @@ import { revocationHandler, RevocationHandlerOptions } from '../../../src/auth/handlers/revoke'; import { OAuthServerProvider, AuthorizationParams } from '../../../src/auth/provider'; import { OAuthRegisteredClientsStore } from '../../../src/auth/clients'; -import { OAuthClientInformationFull, OAuthTokenRevocationRequest, OAuthTokens } from '@modelcontextprotocol/core'; +import { OAuthClientInformationFull, OAuthTokenRevocationRequest, OAuthTokens } from '@modelcontextprotocol/core-internal'; import express, { Response } from 'express'; import supertest from 'supertest'; import { AuthInfo } from '../../../src/auth/types'; diff --git a/packages/server-legacy/test/auth/handlers/token.test.ts b/packages/server-legacy/test/auth/handlers/token.test.ts index 88ab66e684..ade2aca5ec 100644 --- a/packages/server-legacy/test/auth/handlers/token.test.ts +++ b/packages/server-legacy/test/auth/handlers/token.test.ts @@ -1,7 +1,7 @@ import { tokenHandler, TokenHandlerOptions } from '../../../src/auth/handlers/token'; import { OAuthServerProvider, AuthorizationParams } from '../../../src/auth/provider'; import { OAuthRegisteredClientsStore } from '../../../src/auth/clients'; -import { OAuthClientInformationFull, OAuthTokenRevocationRequest, OAuthTokens } from '@modelcontextprotocol/core'; +import { OAuthClientInformationFull, OAuthTokenRevocationRequest, OAuthTokens } from '@modelcontextprotocol/core-internal'; import express, { Response } from 'express'; import supertest from 'supertest'; import * as pkceChallenge from 'pkce-challenge'; diff --git a/packages/server-legacy/test/auth/middleware/clientAuth.test.ts b/packages/server-legacy/test/auth/middleware/clientAuth.test.ts index 82aaa23281..df21c5d205 100644 --- a/packages/server-legacy/test/auth/middleware/clientAuth.test.ts +++ b/packages/server-legacy/test/auth/middleware/clientAuth.test.ts @@ -1,6 +1,6 @@ import { authenticateClient, ClientAuthenticationMiddlewareOptions } from '../../../src/auth/middleware/clientAuth'; import { OAuthRegisteredClientsStore } from '../../../src/auth/clients'; -import { OAuthClientInformationFull } from '@modelcontextprotocol/core'; +import { OAuthClientInformationFull } from '@modelcontextprotocol/core-internal'; import express from 'express'; import supertest from 'supertest'; diff --git a/packages/server-legacy/test/auth/providers/proxyProvider.test.ts b/packages/server-legacy/test/auth/providers/proxyProvider.test.ts index ed2b2b8651..75bb3c51f3 100644 --- a/packages/server-legacy/test/auth/providers/proxyProvider.test.ts +++ b/packages/server-legacy/test/auth/providers/proxyProvider.test.ts @@ -1,7 +1,7 @@ import { Response } from 'express'; import { ProxyOAuthServerProvider, ProxyOptions } from '../../../src/auth/providers/proxyProvider'; import { AuthInfo } from '../../../src/auth/types'; -import { OAuthClientInformationFull, OAuthTokens } from '@modelcontextprotocol/core'; +import { OAuthClientInformationFull, OAuthTokens } from '@modelcontextprotocol/core-internal'; import { ServerError } from '../../../src/auth/errors'; import { InvalidTokenError } from '../../../src/auth/errors'; import { InsufficientScopeError } from '../../../src/auth/errors'; diff --git a/packages/server-legacy/test/auth/router.test.ts b/packages/server-legacy/test/auth/router.test.ts index 1e21f2cf5e..321ebfaf3f 100644 --- a/packages/server-legacy/test/auth/router.test.ts +++ b/packages/server-legacy/test/auth/router.test.ts @@ -1,7 +1,7 @@ import { mcpAuthRouter, AuthRouterOptions, mcpAuthMetadataRouter, AuthMetadataOptions } from '../../src/auth/router'; import { OAuthServerProvider, AuthorizationParams } from '../../src/auth/provider'; import { OAuthRegisteredClientsStore } from '../../src/auth/clients'; -import { OAuthClientInformationFull, OAuthMetadata, OAuthTokenRevocationRequest, OAuthTokens } from '@modelcontextprotocol/core'; +import { OAuthClientInformationFull, OAuthMetadata, OAuthTokenRevocationRequest, OAuthTokens } from '@modelcontextprotocol/core-internal'; import express, { Response } from 'express'; import supertest from 'supertest'; import { AuthInfo } from '../../src/auth/types'; diff --git a/packages/server-legacy/test/sse/sse.test.ts b/packages/server-legacy/test/sse/sse.test.ts index c9e17ea010..212ad7f958 100644 --- a/packages/server-legacy/test/sse/sse.test.ts +++ b/packages/server-legacy/test/sse/sse.test.ts @@ -2,7 +2,7 @@ import http from 'node:http'; import { type Mocked } from 'vitest'; import { SSEServerTransport } from '../../src/sse/sse'; -import type { JSONRPCMessage } from '@modelcontextprotocol/core'; +import type { JSONRPCMessage } from '@modelcontextprotocol/core-internal'; const createMockResponse = () => { const res = { diff --git a/packages/server-legacy/tsconfig.json b/packages/server-legacy/tsconfig.json index 18c1327cbc..e70d31d788 100644 --- a/packages/server-legacy/tsconfig.json +++ b/packages/server-legacy/tsconfig.json @@ -5,8 +5,8 @@ "compilerOptions": { "paths": { "*": ["./*"], - "@modelcontextprotocol/core": ["./node_modules/@modelcontextprotocol/core/src/index.ts"], - "@modelcontextprotocol/core/public": ["./node_modules/@modelcontextprotocol/core/src/exports/public/index.ts"] + "@modelcontextprotocol/core-internal": ["./node_modules/@modelcontextprotocol/core-internal/src/index.ts"], + "@modelcontextprotocol/core-internal/public": ["./node_modules/@modelcontextprotocol/core-internal/src/exports/public/index.ts"] } } } diff --git a/packages/server-legacy/tsdown.config.ts b/packages/server-legacy/tsdown.config.ts index 458d92e8a0..2fff3afcd7 100644 --- a/packages/server-legacy/tsdown.config.ts +++ b/packages/server-legacy/tsdown.config.ts @@ -15,10 +15,10 @@ export default defineConfig({ compilerOptions: { baseUrl: '.', paths: { - '@modelcontextprotocol/core': ['../core/src/index.ts'], - '@modelcontextprotocol/core/public': ['../core/src/exports/public/index.ts'] + '@modelcontextprotocol/core-internal': ['../core-internal/src/index.ts'], + '@modelcontextprotocol/core-internal/public': ['../core-internal/src/exports/public/index.ts'] } } }, - noExternal: ['@modelcontextprotocol/core'] + noExternal: ['@modelcontextprotocol/core-internal'] }); diff --git a/packages/server/CHANGELOG.md b/packages/server/CHANGELOG.md index 3f27e84e5a..26cd457c61 100644 --- a/packages/server/CHANGELOG.md +++ b/packages/server/CHANGELOG.md @@ -51,7 +51,7 @@ For raw JSON Schema (e.g. TypeBox output), use the new `fromJsonSchema` adapter: ```typescript - import { fromJsonSchema, AjvJsonSchemaValidator } from '@modelcontextprotocol/core'; + import { fromJsonSchema, AjvJsonSchemaValidator } from '@modelcontextprotocol/core-internal'; server.registerTool( 'greet', @@ -64,7 +64,8 @@ **Breaking changes:** - `experimental.tasks.getTaskResult()` no longer accepts a `resultSchema` parameter. Returns `GetTaskPayloadResult` (a loose `Result`); cast to the expected type at the call site. - - Removed unused exports from `@modelcontextprotocol/core`: `SchemaInput`, `schemaToJson`, `parseSchemaAsync`, `getSchemaShape`, `getSchemaDescription`, `isOptionalSchema`, `unwrapOptionalSchema`. Use the new `standardSchemaToJsonSchema` and `validateStandardSchema` instead. + - Removed unused exports from `@modelcontextprotocol/core-internal`: `SchemaInput`, `schemaToJson`, `parseSchemaAsync`, `getSchemaShape`, `getSchemaDescription`, `isOptionalSchema`, `unwrapOptionalSchema`. Use the new `standardSchemaToJsonSchema` and `validateStandardSchema` + instead. - `completable()` remains Zod-specific (it relies on Zod's `.shape` introspection). ### Patch Changes diff --git a/packages/server/eslint.config.mjs b/packages/server/eslint.config.mjs index 4f034f2235..dd9e88588a 100644 --- a/packages/server/eslint.config.mjs +++ b/packages/server/eslint.config.mjs @@ -6,7 +6,7 @@ export default [ ...baseConfig, { settings: { - 'import/internal-regex': '^@modelcontextprotocol/core' + 'import/internal-regex': '^@modelcontextprotocol/core-internal' } } ]; diff --git a/packages/server/package.json b/packages/server/package.json index 4a058cbd7b..ef3f05146d 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -94,7 +94,7 @@ "ajv": "catalog:runtimeShared", "ajv-formats": "catalog:runtimeShared", "@eslint/js": "catalog:devTools", - "@modelcontextprotocol/core": "workspace:^", + "@modelcontextprotocol/core-internal": "workspace:^", "@modelcontextprotocol/eslint-config": "workspace:^", "@modelcontextprotocol/test-helpers": "workspace:^", "@modelcontextprotocol/tsconfig": "workspace:^", diff --git a/packages/server/src/fromJsonSchema.ts b/packages/server/src/fromJsonSchema.ts index 180ef2defc..c2db3fda44 100644 --- a/packages/server/src/fromJsonSchema.ts +++ b/packages/server/src/fromJsonSchema.ts @@ -1,5 +1,5 @@ -import type { JsonSchemaType, jsonSchemaValidator, StandardSchemaWithJSON } from '@modelcontextprotocol/core'; -import { fromJsonSchema as coreFromJsonSchema } from '@modelcontextprotocol/core'; +import type { JsonSchemaType, jsonSchemaValidator, StandardSchemaWithJSON } from '@modelcontextprotocol/core-internal'; +import { fromJsonSchema as coreFromJsonSchema } from '@modelcontextprotocol/core-internal'; import { DefaultJsonSchemaValidator } from '@modelcontextprotocol/server/_shims'; let _defaultValidator: jsonSchemaValidator | undefined; diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index d74972e140..292c35abad 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -2,7 +2,7 @@ // // This file defines the complete public surface. It consists of: // - Package-specific exports: listed explicitly below (named imports) -// - Protocol-level types: re-exported from @modelcontextprotocol/core/public +// - Protocol-level types: re-exported from @modelcontextprotocol/core-internal/public // // Any new export added here becomes public API. Use named exports, not wildcards. @@ -44,4 +44,4 @@ export { WebStandardStreamableHTTPServerTransport } from './server/streamableHtt export { fromJsonSchema } from './fromJsonSchema'; // re-export curated public API from core -export * from '@modelcontextprotocol/core/public'; +export * from '@modelcontextprotocol/core-internal/public'; diff --git a/packages/server/src/server/completable.ts b/packages/server/src/server/completable.ts index 82300f7df1..b8ab7b6b68 100644 --- a/packages/server/src/server/completable.ts +++ b/packages/server/src/server/completable.ts @@ -1,4 +1,4 @@ -import type { StandardSchemaV1 } from '@modelcontextprotocol/core'; +import type { StandardSchemaV1 } from '@modelcontextprotocol/core-internal'; export const COMPLETABLE_SYMBOL: unique symbol = Symbol.for('mcp.completable'); diff --git a/packages/server/src/server/mcp.examples.ts b/packages/server/src/server/mcp.examples.ts index 7c30582e81..c4abb9ccb7 100644 --- a/packages/server/src/server/mcp.examples.ts +++ b/packages/server/src/server/mcp.examples.ts @@ -7,7 +7,7 @@ * @module */ -import type { CallToolResult } from '@modelcontextprotocol/core'; +import type { CallToolResult } from '@modelcontextprotocol/core-internal'; import * as z from 'zod/v4'; import { McpServer } from './mcp'; diff --git a/packages/server/src/server/mcp.ts b/packages/server/src/server/mcp.ts index b93d92d589..be18a7c7cd 100644 --- a/packages/server/src/server/mcp.ts +++ b/packages/server/src/server/mcp.ts @@ -24,7 +24,7 @@ import type { ToolExecution, Transport, Variables -} from '@modelcontextprotocol/core'; +} from '@modelcontextprotocol/core-internal'; import { assertCompleteRequestPrompt, assertCompleteRequestResourceTemplate, @@ -36,7 +36,7 @@ import { UriTemplate, validateAndWarnToolName, validateStandardSchema -} from '@modelcontextprotocol/core'; +} from '@modelcontextprotocol/core-internal'; import type * as z from 'zod/v4'; import { getCompleter, isCompletable } from './completable'; diff --git a/packages/server/src/server/server.ts b/packages/server/src/server/server.ts index f96d8ec1bc..b0777d118b 100644 --- a/packages/server/src/server/server.ts +++ b/packages/server/src/server/server.ts @@ -30,7 +30,7 @@ import type { ServerContext, ToolResultContent, ToolUseContent -} from '@modelcontextprotocol/core'; +} from '@modelcontextprotocol/core-internal'; import { CallToolRequestSchema, CallToolResultSchema, @@ -48,7 +48,7 @@ import { ProtocolErrorCode, SdkError, SdkErrorCode -} from '@modelcontextprotocol/core'; +} from '@modelcontextprotocol/core-internal'; import { DefaultJsonSchemaValidator } from '@modelcontextprotocol/server/_shims'; export type ServerOptions = ProtocolOptions & { diff --git a/packages/server/src/server/stdio.ts b/packages/server/src/server/stdio.ts index a790fc75a4..e8fda03257 100644 --- a/packages/server/src/server/stdio.ts +++ b/packages/server/src/server/stdio.ts @@ -1,7 +1,7 @@ import type { Readable, Writable } from 'node:stream'; -import type { JSONRPCMessage, Transport } from '@modelcontextprotocol/core'; -import { ReadBuffer, serializeMessage } from '@modelcontextprotocol/core'; +import type { JSONRPCMessage, Transport } from '@modelcontextprotocol/core-internal'; +import { ReadBuffer, serializeMessage } from '@modelcontextprotocol/core-internal'; import { process } from '@modelcontextprotocol/server/_shims'; /** diff --git a/packages/server/src/server/streamableHttp.ts b/packages/server/src/server/streamableHttp.ts index fb6ffc2b02..9b6c4d9555 100644 --- a/packages/server/src/server/streamableHttp.ts +++ b/packages/server/src/server/streamableHttp.ts @@ -7,7 +7,7 @@ * For Node.js Express/HTTP compatibility, use {@linkcode @modelcontextprotocol/node!NodeStreamableHTTPServerTransport | NodeStreamableHTTPServerTransport} which wraps this transport. */ -import type { AuthInfo, JSONRPCMessage, MessageExtraInfo, RequestId, Transport } from '@modelcontextprotocol/core'; +import type { AuthInfo, JSONRPCMessage, MessageExtraInfo, RequestId, Transport } from '@modelcontextprotocol/core-internal'; import { DEFAULT_NEGOTIATED_PROTOCOL_VERSION, isInitializeRequest, @@ -16,7 +16,7 @@ import { isJSONRPCResultResponse, JSONRPCMessageSchema, SUPPORTED_PROTOCOL_VERSIONS -} from '@modelcontextprotocol/core'; +} from '@modelcontextprotocol/core-internal'; export type StreamId = string; export type EventId = string; diff --git a/packages/server/src/shimsNode.ts b/packages/server/src/shimsNode.ts index 9354850b6e..6ec703659a 100644 --- a/packages/server/src/shimsNode.ts +++ b/packages/server/src/shimsNode.ts @@ -3,5 +3,5 @@ * * This file is selected via package.json export conditions when running in Node.js. */ -export { AjvJsonSchemaValidator as DefaultJsonSchemaValidator } from '@modelcontextprotocol/core/validators/ajv'; +export { AjvJsonSchemaValidator as DefaultJsonSchemaValidator } from '@modelcontextprotocol/core-internal/validators/ajv'; export { default as process } from 'node:process'; diff --git a/packages/server/src/shimsWorkerd.ts b/packages/server/src/shimsWorkerd.ts index dc23da8080..92e3bf0f17 100644 --- a/packages/server/src/shimsWorkerd.ts +++ b/packages/server/src/shimsWorkerd.ts @@ -3,7 +3,7 @@ * * This file is selected via package.json export conditions when running in workerd. */ -export { CfWorkerJsonSchemaValidator as DefaultJsonSchemaValidator } from '@modelcontextprotocol/core/validators/cfWorker'; +export { CfWorkerJsonSchemaValidator as DefaultJsonSchemaValidator } from '@modelcontextprotocol/core-internal/validators/cfWorker'; /** * Stub process object for non-Node.js environments. diff --git a/packages/server/src/validators/ajv.ts b/packages/server/src/validators/ajv.ts index 31f0fed3b3..8bd1a78a8d 100644 --- a/packages/server/src/validators/ajv.ts +++ b/packages/server/src/validators/ajv.ts @@ -11,4 +11,4 @@ * const validator = new AjvJsonSchemaValidator(ajv); * ``` */ -export { addFormats, Ajv, AjvJsonSchemaValidator } from '@modelcontextprotocol/core/validators/ajv'; +export { addFormats, Ajv, AjvJsonSchemaValidator } from '@modelcontextprotocol/core-internal/validators/ajv'; diff --git a/packages/server/src/validators/cfWorker.ts b/packages/server/src/validators/cfWorker.ts index 2969b4dc9d..f1d9379afc 100644 --- a/packages/server/src/validators/cfWorker.ts +++ b/packages/server/src/validators/cfWorker.ts @@ -1,3 +1,3 @@ /** Customisation entry point for the `@cfworker/json-schema` validator. */ -export type { CfWorkerSchemaDraft } from '@modelcontextprotocol/core/validators/cfWorker'; -export { CfWorkerJsonSchemaValidator } from '@modelcontextprotocol/core/validators/cfWorker'; +export type { CfWorkerSchemaDraft } from '@modelcontextprotocol/core-internal/validators/cfWorker'; +export { CfWorkerJsonSchemaValidator } from '@modelcontextprotocol/core-internal/validators/cfWorker'; diff --git a/packages/server/test/server/jsonSchemaValidatorOverride.test.ts b/packages/server/test/server/jsonSchemaValidatorOverride.test.ts index 2931f8aa00..e5edf18398 100644 --- a/packages/server/test/server/jsonSchemaValidatorOverride.test.ts +++ b/packages/server/test/server/jsonSchemaValidatorOverride.test.ts @@ -1,5 +1,5 @@ -import type { JsonSchemaType, JsonSchemaValidatorResult, jsonSchemaValidator } from '@modelcontextprotocol/core'; -import { InMemoryTransport, LATEST_PROTOCOL_VERSION } from '@modelcontextprotocol/core'; +import type { JsonSchemaType, JsonSchemaValidatorResult, jsonSchemaValidator } from '@modelcontextprotocol/core-internal'; +import { InMemoryTransport, LATEST_PROTOCOL_VERSION } from '@modelcontextprotocol/core-internal'; import { fromJsonSchema } from '../../src/fromJsonSchema'; import { Server } from '../../src/server/server'; diff --git a/packages/server/test/server/mcp.compat.test.ts b/packages/server/test/server/mcp.compat.test.ts index 2b6e960bb4..6c87e25f12 100644 --- a/packages/server/test/server/mcp.compat.test.ts +++ b/packages/server/test/server/mcp.compat.test.ts @@ -1,5 +1,5 @@ -import type { JSONRPCMessage } from '@modelcontextprotocol/core'; -import { InMemoryTransport, isStandardSchema, LATEST_PROTOCOL_VERSION } from '@modelcontextprotocol/core'; +import type { JSONRPCMessage } from '@modelcontextprotocol/core-internal'; +import { InMemoryTransport, isStandardSchema, LATEST_PROTOCOL_VERSION } from '@modelcontextprotocol/core-internal'; import { describe, expect, expectTypeOf, it, vi } from 'vitest'; import * as z from 'zod/v4'; import { McpServer } from '../../src/index'; diff --git a/packages/server/test/server/mcp.icons.test.ts b/packages/server/test/server/mcp.icons.test.ts index cbd3f15483..168a6977c0 100644 --- a/packages/server/test/server/mcp.icons.test.ts +++ b/packages/server/test/server/mcp.icons.test.ts @@ -1,5 +1,5 @@ -import type { Icon, JSONRPCMessage } from '@modelcontextprotocol/core'; -import { InMemoryTransport, LATEST_PROTOCOL_VERSION } from '@modelcontextprotocol/core'; +import type { Icon, JSONRPCMessage } from '@modelcontextprotocol/core-internal'; +import { InMemoryTransport, LATEST_PROTOCOL_VERSION } from '@modelcontextprotocol/core-internal'; import { describe, expect, it, vi } from 'vitest'; import { McpServer, ResourceTemplate } from '../../src/index'; diff --git a/packages/server/test/server/server.test.ts b/packages/server/test/server/server.test.ts index 1929197b19..8de735d73a 100644 --- a/packages/server/test/server/server.test.ts +++ b/packages/server/test/server/server.test.ts @@ -1,11 +1,11 @@ -import type { JSONRPCMessage, JSONRPCRequest } from '@modelcontextprotocol/core'; +import type { JSONRPCMessage, JSONRPCRequest } from '@modelcontextprotocol/core-internal'; import { InitializeResultSchema, InMemoryTransport, isJSONRPCResultResponse, LATEST_PROTOCOL_VERSION, SUPPORTED_PROTOCOL_VERSIONS -} from '@modelcontextprotocol/core'; +} from '@modelcontextprotocol/core-internal'; import { Server } from '../../src/server/server'; /** An older protocol version the server supports out of the box. */ diff --git a/packages/server/test/server/stdio.test.ts b/packages/server/test/server/stdio.test.ts index e21143aea5..fe79e3679c 100644 --- a/packages/server/test/server/stdio.test.ts +++ b/packages/server/test/server/stdio.test.ts @@ -1,7 +1,7 @@ import { Readable, Writable } from 'node:stream'; -import type { JSONRPCMessage } from '@modelcontextprotocol/core'; -import { ReadBuffer, serializeMessage } from '@modelcontextprotocol/core'; +import type { JSONRPCMessage } from '@modelcontextprotocol/core-internal'; +import { ReadBuffer, serializeMessage } from '@modelcontextprotocol/core-internal'; import { StdioServerTransport } from '../../src/server/stdio'; diff --git a/packages/server/test/server/streamableHttp.test.ts b/packages/server/test/server/streamableHttp.test.ts index 0fc61ce32a..b9bbbd8f9f 100644 --- a/packages/server/test/server/streamableHttp.test.ts +++ b/packages/server/test/server/streamableHttp.test.ts @@ -1,6 +1,6 @@ import { randomUUID } from 'node:crypto'; -import type { CallToolResult, JSONRPCErrorResponse, JSONRPCMessage } from '@modelcontextprotocol/core'; +import type { CallToolResult, JSONRPCErrorResponse, JSONRPCMessage } from '@modelcontextprotocol/core-internal'; import * as z from 'zod/v4'; import { McpServer } from '../../src/server/mcp'; diff --git a/packages/server/test/server/streamableHttpFutureVersionGates.test.ts b/packages/server/test/server/streamableHttpFutureVersionGates.test.ts index def61b5223..4abf9fb74d 100644 --- a/packages/server/test/server/streamableHttpFutureVersionGates.test.ts +++ b/packages/server/test/server/streamableHttpFutureVersionGates.test.ts @@ -1,6 +1,6 @@ import { randomUUID } from 'node:crypto'; -import type { JSONRPCMessage, MessageExtraInfo } from '@modelcontextprotocol/core'; +import type { JSONRPCMessage, MessageExtraInfo } from '@modelcontextprotocol/core-internal'; import { McpServer } from '../../src/server/mcp'; import type { EventId, EventStore, StreamId } from '../../src/server/streamableHttp'; diff --git a/packages/server/test/server/streamableHttpUnsupportedVersionLiteral.test.ts b/packages/server/test/server/streamableHttpUnsupportedVersionLiteral.test.ts index b098af1fea..d44cc642dc 100644 --- a/packages/server/test/server/streamableHttpUnsupportedVersionLiteral.test.ts +++ b/packages/server/test/server/streamableHttpUnsupportedVersionLiteral.test.ts @@ -1,7 +1,7 @@ import { randomUUID } from 'node:crypto'; -import type { JSONRPCMessage } from '@modelcontextprotocol/core'; -import { SUPPORTED_PROTOCOL_VERSIONS } from '@modelcontextprotocol/core'; +import type { JSONRPCMessage } from '@modelcontextprotocol/core-internal'; +import { SUPPORTED_PROTOCOL_VERSIONS } from '@modelcontextprotocol/core-internal'; import { McpServer } from '../../src/server/mcp'; import { WebStandardStreamableHTTPServerTransport } from '../../src/server/streamableHttp'; diff --git a/packages/server/tsconfig.json b/packages/server/tsconfig.json index 24da6e426d..2d8ef8ed15 100644 --- a/packages/server/tsconfig.json +++ b/packages/server/tsconfig.json @@ -5,11 +5,15 @@ "compilerOptions": { "paths": { "*": ["./*"], - "@modelcontextprotocol/core": ["./node_modules/@modelcontextprotocol/core/src/index.ts"], - "@modelcontextprotocol/core/public": ["./node_modules/@modelcontextprotocol/core/src/exports/public/index.ts"], - "@modelcontextprotocol/core/validators/ajv": ["./node_modules/@modelcontextprotocol/core/src/validators/ajvProvider.ts"], - "@modelcontextprotocol/core/validators/cfWorker": [ - "./node_modules/@modelcontextprotocol/core/src/validators/cfWorkerProvider.ts" + "@modelcontextprotocol/core-internal": ["./node_modules/@modelcontextprotocol/core-internal/src/index.ts"], + "@modelcontextprotocol/core-internal/public": [ + "./node_modules/@modelcontextprotocol/core-internal/src/exports/public/index.ts" + ], + "@modelcontextprotocol/core-internal/validators/ajv": [ + "./node_modules/@modelcontextprotocol/core-internal/src/validators/ajvProvider.ts" + ], + "@modelcontextprotocol/core-internal/validators/cfWorker": [ + "./node_modules/@modelcontextprotocol/core-internal/src/validators/cfWorkerProvider.ts" ], "@modelcontextprotocol/test-helpers": ["./node_modules/@modelcontextprotocol/test-helpers/src/index.ts"], "@modelcontextprotocol/server/_shims": ["./src/shimsNode.ts"] diff --git a/packages/server/tsdown.config.ts b/packages/server/tsdown.config.ts index 891ce49641..9908a6e730 100644 --- a/packages/server/tsdown.config.ts +++ b/packages/server/tsdown.config.ts @@ -23,13 +23,13 @@ export default defineConfig({ compilerOptions: { baseUrl: '.', paths: { - '@modelcontextprotocol/core': ['../core/src/index.ts'], - '@modelcontextprotocol/core/public': ['../core/src/exports/public/index.ts'], - '@modelcontextprotocol/core/validators/ajv': ['../core/src/validators/ajvProvider.ts'], - '@modelcontextprotocol/core/validators/cfWorker': ['../core/src/validators/cfWorkerProvider.ts'] + '@modelcontextprotocol/core-internal': ['../core-internal/src/index.ts'], + '@modelcontextprotocol/core-internal/public': ['../core-internal/src/exports/public/index.ts'], + '@modelcontextprotocol/core-internal/validators/ajv': ['../core-internal/src/validators/ajvProvider.ts'], + '@modelcontextprotocol/core-internal/validators/cfWorker': ['../core-internal/src/validators/cfWorkerProvider.ts'] } } }, - noExternal: ['@modelcontextprotocol/core', 'ajv', 'ajv-formats', '@cfworker/json-schema'], + noExternal: ['@modelcontextprotocol/core-internal', 'ajv', 'ajv-formats', '@cfworker/json-schema'], external: ['@modelcontextprotocol/server/_shims'] }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 126830827f..0a971a2ebd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -428,9 +428,9 @@ importers: examples/shared: dependencies: - '@modelcontextprotocol/core': + '@modelcontextprotocol/core-internal': specifier: workspace:^ - version: link:../../packages/core + version: link:../../packages/core-internal '@modelcontextprotocol/express': specifier: workspace:^ version: link:../../packages/middleware/express @@ -526,9 +526,9 @@ importers: '@eslint/js': specifier: catalog:devTools version: 9.39.4 - '@modelcontextprotocol/core': + '@modelcontextprotocol/core-internal': specifier: workspace:^ - version: link:../core + version: link:../core-internal '@modelcontextprotocol/eslint-config': specifier: workspace:^ version: link:../../common/eslint-config @@ -637,6 +637,55 @@ importers: version: 4.1.2(@opentelemetry/api@1.9.1)(@types/node@25.5.0)(vite@7.3.0(@types/node@25.5.0)(tsx@4.21.0)(yaml@2.8.3)) packages/core: + dependencies: + zod: + specifier: catalog:runtimeShared + version: 4.3.6 + devDependencies: + '@eslint/js': + specifier: catalog:devTools + version: 9.39.4 + '@modelcontextprotocol/core-internal': + specifier: workspace:^ + version: link:../core-internal + '@modelcontextprotocol/eslint-config': + specifier: workspace:^ + version: link:../../common/eslint-config + '@modelcontextprotocol/tsconfig': + specifier: workspace:^ + version: link:../../common/tsconfig + '@modelcontextprotocol/vitest-config': + specifier: workspace:^ + version: link:../../common/vitest-config + '@typescript/native-preview': + specifier: catalog:devTools + version: 7.0.0-dev.20260327.2 + eslint: + specifier: catalog:devTools + version: 9.39.4 + eslint-config-prettier: + specifier: catalog:devTools + version: 10.1.8(eslint@9.39.4) + eslint-plugin-n: + specifier: catalog:devTools + version: 17.24.0(eslint@9.39.4)(typescript@5.9.3) + prettier: + specifier: catalog:devTools + version: 3.6.2 + tsdown: + specifier: catalog:devTools + version: 0.18.4(@typescript/native-preview@7.0.0-dev.20260327.2)(typescript@5.9.3) + typescript: + specifier: catalog:devTools + version: 5.9.3 + typescript-eslint: + specifier: catalog:devTools + version: 8.57.2(eslint@9.39.4)(typescript@5.9.3) + vitest: + specifier: catalog:devTools + version: 4.1.2(@opentelemetry/api@1.9.1)(@types/node@25.5.0)(vite@7.3.0(@types/node@25.5.0)(tsx@4.21.0)(yaml@2.8.3)) + + packages/core-internal: dependencies: json-schema-typed: specifier: catalog:runtimeShared @@ -886,9 +935,9 @@ importers: '@eslint/js': specifier: catalog:devTools version: 9.39.4 - '@modelcontextprotocol/core': + '@modelcontextprotocol/core-internal': specifier: workspace:^ - version: link:../../core + version: link:../../core-internal '@modelcontextprotocol/eslint-config': specifier: workspace:^ version: link:../../../common/eslint-config @@ -932,55 +981,6 @@ importers: specifier: catalog:devTools version: 4.1.2(@opentelemetry/api@1.9.1)(@types/node@25.5.0)(vite@7.3.0(@types/node@25.5.0)(tsx@4.21.0)(yaml@2.8.3)) - packages/sdk-shared: - dependencies: - zod: - specifier: catalog:runtimeShared - version: 4.3.6 - devDependencies: - '@eslint/js': - specifier: catalog:devTools - version: 9.39.4 - '@modelcontextprotocol/core': - specifier: workspace:^ - version: link:../core - '@modelcontextprotocol/eslint-config': - specifier: workspace:^ - version: link:../../common/eslint-config - '@modelcontextprotocol/tsconfig': - specifier: workspace:^ - version: link:../../common/tsconfig - '@modelcontextprotocol/vitest-config': - specifier: workspace:^ - version: link:../../common/vitest-config - '@typescript/native-preview': - specifier: catalog:devTools - version: 7.0.0-dev.20260327.2 - eslint: - specifier: catalog:devTools - version: 9.39.4 - eslint-config-prettier: - specifier: catalog:devTools - version: 10.1.8(eslint@9.39.4) - eslint-plugin-n: - specifier: catalog:devTools - version: 17.24.0(eslint@9.39.4)(typescript@5.9.3) - prettier: - specifier: catalog:devTools - version: 3.6.2 - tsdown: - specifier: catalog:devTools - version: 0.18.4(@typescript/native-preview@7.0.0-dev.20260327.2)(typescript@5.9.3) - typescript: - specifier: catalog:devTools - version: 5.9.3 - typescript-eslint: - specifier: catalog:devTools - version: 8.57.2(eslint@9.39.4)(typescript@5.9.3) - vitest: - specifier: catalog:devTools - version: 4.1.2(@opentelemetry/api@1.9.1)(@types/node@25.5.0)(vite@7.3.0(@types/node@25.5.0)(tsx@4.21.0)(yaml@2.8.3)) - packages/server: dependencies: zod: @@ -993,9 +993,9 @@ importers: '@eslint/js': specifier: catalog:devTools version: 9.39.4 - '@modelcontextprotocol/core': + '@modelcontextprotocol/core-internal': specifier: workspace:^ - version: link:../core + version: link:../core-internal '@modelcontextprotocol/eslint-config': specifier: workspace:^ version: link:../../common/eslint-config @@ -1075,9 +1075,9 @@ importers: '@eslint/js': specifier: catalog:devTools version: 9.39.4 - '@modelcontextprotocol/core': + '@modelcontextprotocol/core-internal': specifier: workspace:^ - version: link:../core + version: link:../core-internal '@modelcontextprotocol/eslint-config': specifier: workspace:^ version: link:../../common/eslint-config @@ -1141,9 +1141,9 @@ importers: '@modelcontextprotocol/conformance': specifier: 0.2.0-alpha.3 version: 0.2.0-alpha.3(@cfworker/json-schema@4.1.1) - '@modelcontextprotocol/core': + '@modelcontextprotocol/core-internal': specifier: workspace:^ - version: link:../../packages/core + version: link:../../packages/core-internal '@modelcontextprotocol/eslint-config': specifier: workspace:^ version: link:../../common/eslint-config @@ -1186,9 +1186,9 @@ importers: '@modelcontextprotocol/client': specifier: workspace:^ version: link:../../packages/client - '@modelcontextprotocol/core': + '@modelcontextprotocol/core-internal': specifier: workspace:^ - version: link:../../packages/core + version: link:../../packages/core-internal '@modelcontextprotocol/eslint-config': specifier: workspace:^ version: link:../../common/eslint-config @@ -1264,9 +1264,9 @@ importers: test/helpers: devDependencies: - '@modelcontextprotocol/core': + '@modelcontextprotocol/core-internal': specifier: workspace:^ - version: link:../../packages/core + version: link:../../packages/core-internal '@modelcontextprotocol/eslint-config': specifier: workspace:^ version: link:../../common/eslint-config @@ -1291,9 +1291,9 @@ importers: '@modelcontextprotocol/client': specifier: workspace:^ version: link:../../packages/client - '@modelcontextprotocol/core': + '@modelcontextprotocol/core-internal': specifier: workspace:^ - version: link:../../packages/core + version: link:../../packages/core-internal '@modelcontextprotocol/eslint-config': specifier: workspace:^ version: link:../../common/eslint-config diff --git a/scripts/fetch-spec-types.ts b/scripts/fetch-spec-types.ts index b0db8d486f..90921276f2 100644 --- a/scripts/fetch-spec-types.ts +++ b/scripts/fetch-spec-types.ts @@ -12,7 +12,7 @@ const PROJECT_ROOT = join(__dirname, '..'); * - `2025-11-25`: the frozen, released schema. * - `2026-07-28`: the upcoming protocol revision. * - * Each is written to `packages/core/src/types/spec.types..ts`. + * Each is written to `packages/core-internal/src/types/spec.types..ts`. */ const SUPPORTED_VERSIONS = ['2025-11-25', '2026-07-28'] as const; type SpecVersion = (typeof SUPPORTED_VERSIONS)[number]; @@ -96,7 +96,7 @@ async function updateSpecTypes(version: SpecVersion, providedSHA?: string): Prom writeFileSync(outputPath, formatted, 'utf-8'); - console.log(`[${version}] Successfully updated packages/core/src/types/spec.types.${version}.ts`); + console.log(`[${version}] Successfully updated packages/core-internal/src/types/spec.types.${version}.ts`); } function isSupportedVersion(value: string): value is SpecVersion { diff --git a/scripts/sync-snippets.ts b/scripts/sync-snippets.ts index 21a2c4e70b..6d5d81410c 100644 --- a/scripts/sync-snippets.ts +++ b/scripts/sync-snippets.ts @@ -516,7 +516,7 @@ function findPackageSrcDirs(packagesDir: string): string[] { const fullPath = join(entry.parentPath, entry.name); // Only include src dirs that are direct children of a package - // (e.g., packages/core/src, packages/middleware/express/src) + // (e.g., packages/core-internal/src, packages/middleware/express/src) // Skip nested src dirs like node_modules/*/src if (fullPath.includes('node_modules')) continue; diff --git a/test/conformance/package.json b/test/conformance/package.json index f4f927096c..09c54234a6 100644 --- a/test/conformance/package.json +++ b/test/conformance/package.json @@ -38,7 +38,7 @@ "@modelcontextprotocol/conformance": "0.2.0-alpha.3", "@modelcontextprotocol/client": "workspace:^", "@modelcontextprotocol/server": "workspace:^", - "@modelcontextprotocol/core": "workspace:^", + "@modelcontextprotocol/core-internal": "workspace:^", "@modelcontextprotocol/express": "workspace:^", "@modelcontextprotocol/node": "workspace:^", "@modelcontextprotocol/tsconfig": "workspace:^", diff --git a/test/conformance/tsconfig.json b/test/conformance/tsconfig.json index f529719742..dffb32355b 100644 --- a/test/conformance/tsconfig.json +++ b/test/conformance/tsconfig.json @@ -5,8 +5,10 @@ "compilerOptions": { "paths": { "*": ["./*"], - "@modelcontextprotocol/core": ["./node_modules/@modelcontextprotocol/core/src/index.ts"], - "@modelcontextprotocol/core/public": ["./node_modules/@modelcontextprotocol/core/src/exports/public/index.ts"], + "@modelcontextprotocol/core-internal": ["./node_modules/@modelcontextprotocol/core-internal/src/index.ts"], + "@modelcontextprotocol/core-internal/public": [ + "./node_modules/@modelcontextprotocol/core-internal/src/exports/public/index.ts" + ], "@modelcontextprotocol/client": ["./node_modules/@modelcontextprotocol/client/src/index.ts"], "@modelcontextprotocol/server": ["./node_modules/@modelcontextprotocol/server/src/index.ts"], "@modelcontextprotocol/express": ["./node_modules/@modelcontextprotocol/express/src/index.ts"], diff --git a/test/e2e/helpers/wire-sniffer.ts b/test/e2e/helpers/wire-sniffer.ts index 89663214ce..03fde0b5af 100644 --- a/test/e2e/helpers/wire-sniffer.ts +++ b/test/e2e/helpers/wire-sniffer.ts @@ -5,7 +5,7 @@ import { ServerNotificationSchema, ServerRequestSchema, ServerResultSchema -} from '@modelcontextprotocol/core'; +} from '@modelcontextprotocol/core-internal'; import type { Transport } from '@modelcontextprotocol/server'; import { isJSONRPCErrorResponse, diff --git a/test/e2e/package.json b/test/e2e/package.json index 2b24c771f7..21460c0651 100644 --- a/test/e2e/package.json +++ b/test/e2e/package.json @@ -32,7 +32,7 @@ "devDependencies": { "@hono/node-server": "catalog:runtimeServerOnly", "@modelcontextprotocol/client": "workspace:^", - "@modelcontextprotocol/core": "workspace:^", + "@modelcontextprotocol/core-internal": "workspace:^", "@modelcontextprotocol/server": "workspace:^", "@modelcontextprotocol/server-legacy": "workspace:^", "@modelcontextprotocol/express": "workspace:^", diff --git a/test/e2e/scenarios/sampling.test.ts b/test/e2e/scenarios/sampling.test.ts index acec14413d..d1b04893b8 100644 --- a/test/e2e/scenarios/sampling.test.ts +++ b/test/e2e/scenarios/sampling.test.ts @@ -10,7 +10,7 @@ import type { ClientCapabilities } from '@modelcontextprotocol/client'; import { Client } from '@modelcontextprotocol/client'; -import { CreateMessageRequestParamsSchema } from '@modelcontextprotocol/core'; +import { CreateMessageRequestParamsSchema } from '@modelcontextprotocol/core-internal'; import type { CreateMessageRequest, CreateMessageResultWithTools, ServerOptions } from '@modelcontextprotocol/server'; import { McpServer, ProtocolError, ProtocolErrorCode } from '@modelcontextprotocol/server'; import { expect } from 'vitest'; diff --git a/test/e2e/scenarios/stdio.test.ts b/test/e2e/scenarios/stdio.test.ts index f066dde648..ba4baf69f3 100644 --- a/test/e2e/scenarios/stdio.test.ts +++ b/test/e2e/scenarios/stdio.test.ts @@ -17,7 +17,7 @@ import { fileURLToPath, pathToFileURL } from 'node:url'; import { Client } from '@modelcontextprotocol/client'; import { StdioClientTransport } from '@modelcontextprotocol/client/stdio'; -import { JSONRPCMessageSchema } from '@modelcontextprotocol/core'; +import { JSONRPCMessageSchema } from '@modelcontextprotocol/core-internal'; import { expect, vi } from 'vitest'; import { verifies } from '../helpers/verifies'; diff --git a/test/e2e/scenarios/tools.test.ts b/test/e2e/scenarios/tools.test.ts index cc12a54b1b..195aa0ce07 100644 --- a/test/e2e/scenarios/tools.test.ts +++ b/test/e2e/scenarios/tools.test.ts @@ -21,7 +21,7 @@ */ import { Client } from '@modelcontextprotocol/client'; -import type { JsonSchemaType } from '@modelcontextprotocol/core'; +import type { JsonSchemaType } from '@modelcontextprotocol/core-internal'; import type { CreateMessageRequest, CreateMessageResult, diff --git a/test/e2e/scenarios/transport-http.test.ts b/test/e2e/scenarios/transport-http.test.ts index 526e554fa2..760df8ea76 100644 --- a/test/e2e/scenarios/transport-http.test.ts +++ b/test/e2e/scenarios/transport-http.test.ts @@ -19,7 +19,7 @@ import { randomUUID } from 'node:crypto'; import { Client, SdkHttpError, StreamableHTTPClientTransport } from '@modelcontextprotocol/client'; -import { JSONRPCRequestSchema } from '@modelcontextprotocol/core'; +import { JSONRPCRequestSchema } from '@modelcontextprotocol/core-internal'; import { LATEST_PROTOCOL_VERSION, McpServer, diff --git a/test/e2e/scenarios/transport-raw.test.ts b/test/e2e/scenarios/transport-raw.test.ts index 7a6aa4fa34..0a14eb1866 100644 --- a/test/e2e/scenarios/transport-raw.test.ts +++ b/test/e2e/scenarios/transport-raw.test.ts @@ -17,7 +17,7 @@ import { fileURLToPath } from 'node:url'; import { StreamableHTTPClientTransport } from '@modelcontextprotocol/client'; import { StdioClientTransport } from '@modelcontextprotocol/client/stdio'; -import { CallToolResultSchema, InitializeResultSchema, JSONRPCResultResponseSchema } from '@modelcontextprotocol/core'; +import { CallToolResultSchema, InitializeResultSchema, JSONRPCResultResponseSchema } from '@modelcontextprotocol/core-internal'; import type { JSONRPCMessage, JSONRPCNotification, JSONRPCRequest } from '@modelcontextprotocol/server'; import { InMemoryTransport, McpServer } from '@modelcontextprotocol/server'; import { expect, vi } from 'vitest'; diff --git a/test/e2e/scenarios/validation.test.ts b/test/e2e/scenarios/validation.test.ts index dfdce32d63..2f2d638cb4 100644 --- a/test/e2e/scenarios/validation.test.ts +++ b/test/e2e/scenarios/validation.test.ts @@ -13,7 +13,7 @@ */ import { Client } from '@modelcontextprotocol/client'; -import type { JsonSchemaType, JsonSchemaValidator, jsonSchemaValidator, StandardSchemaWithJSON } from '@modelcontextprotocol/core'; +import type { JsonSchemaType, JsonSchemaValidator, jsonSchemaValidator, StandardSchemaWithJSON } from '@modelcontextprotocol/core-internal'; import type { Tool } from '@modelcontextprotocol/server'; import { fromJsonSchema, diff --git a/test/e2e/tsconfig.json b/test/e2e/tsconfig.json index 453684bc8b..e8c0fc8ba1 100644 --- a/test/e2e/tsconfig.json +++ b/test/e2e/tsconfig.json @@ -5,10 +5,12 @@ "compilerOptions": { "paths": { "*": ["./*"], - "@modelcontextprotocol/core": ["./node_modules/@modelcontextprotocol/core/src/index.ts"], - "@modelcontextprotocol/core/public": ["./node_modules/@modelcontextprotocol/core/src/exports/public/index.ts"], - "@modelcontextprotocol/core/validators/cfWorker": [ - "./node_modules/@modelcontextprotocol/core/src/validators/cfWorkerProvider.ts" + "@modelcontextprotocol/core-internal": ["./node_modules/@modelcontextprotocol/core-internal/src/index.ts"], + "@modelcontextprotocol/core-internal/public": [ + "./node_modules/@modelcontextprotocol/core-internal/src/exports/public/index.ts" + ], + "@modelcontextprotocol/core-internal/validators/cfWorker": [ + "./node_modules/@modelcontextprotocol/core-internal/src/validators/cfWorkerProvider.ts" ], "@modelcontextprotocol/client": ["./node_modules/@modelcontextprotocol/client/src/index.ts"], "@modelcontextprotocol/client/stdio": ["./node_modules/@modelcontextprotocol/client/src/stdio.ts"], diff --git a/test/helpers/package.json b/test/helpers/package.json index 1826205a1b..3b7e49950e 100644 --- a/test/helpers/package.json +++ b/test/helpers/package.json @@ -27,7 +27,7 @@ "check": "npm run typecheck && npm run lint" }, "devDependencies": { - "@modelcontextprotocol/core": "workspace:^", + "@modelcontextprotocol/core-internal": "workspace:^", "zod": "catalog:runtimeShared", "vitest": "catalog:devTools", "@modelcontextprotocol/tsconfig": "workspace:^", diff --git a/test/helpers/src/helpers/oauth.ts b/test/helpers/src/helpers/oauth.ts index 61c3d44e40..a689b33909 100644 --- a/test/helpers/src/helpers/oauth.ts +++ b/test/helpers/src/helpers/oauth.ts @@ -1,4 +1,4 @@ -import type { FetchLike } from '@modelcontextprotocol/core'; +import type { FetchLike } from '@modelcontextprotocol/core-internal'; import { vi } from 'vitest'; export interface MockOAuthFetchOptions { diff --git a/test/helpers/tsconfig.json b/test/helpers/tsconfig.json index ce44773946..18809d7ac8 100644 --- a/test/helpers/tsconfig.json +++ b/test/helpers/tsconfig.json @@ -5,8 +5,10 @@ "compilerOptions": { "paths": { "*": ["./*"], - "@modelcontextprotocol/core": ["./node_modules/@modelcontextprotocol/core/src/index.ts"], - "@modelcontextprotocol/core/public": ["./node_modules/@modelcontextprotocol/core/src/exports/public/index.ts"], + "@modelcontextprotocol/core-internal": ["./node_modules/@modelcontextprotocol/core-internal/src/index.ts"], + "@modelcontextprotocol/core-internal/public": [ + "./node_modules/@modelcontextprotocol/core-internal/src/exports/public/index.ts" + ], "@modelcontextprotocol/vitest-config": ["./node_modules/@modelcontextprotocol/vitest-config/tsconfig.json"] } } diff --git a/test/integration/package.json b/test/integration/package.json index 97a62e6c32..10997c5be3 100644 --- a/test/integration/package.json +++ b/test/integration/package.json @@ -33,7 +33,7 @@ "devDependencies": { "@cfworker/json-schema": "catalog:runtimeShared", "@modelcontextprotocol/client": "workspace:^", - "@modelcontextprotocol/core": "workspace:^", + "@modelcontextprotocol/core-internal": "workspace:^", "@modelcontextprotocol/eslint-config": "workspace:^", "@modelcontextprotocol/express": "workspace:^", "@modelcontextprotocol/node": "workspace:^", diff --git a/test/integration/test/client/client.test.ts b/test/integration/test/client/client.test.ts index a7613b24e4..a2f53ff71e 100644 --- a/test/integration/test/client/client.test.ts +++ b/test/integration/test/client/client.test.ts @@ -1,5 +1,5 @@ import { Client, getSupportedElicitationModes } from '@modelcontextprotocol/client'; -import type { Prompt, Resource, Tool, Transport } from '@modelcontextprotocol/core'; +import type { Prompt, Resource, Tool, Transport } from '@modelcontextprotocol/core-internal'; import { InMemoryTransport, LATEST_PROTOCOL_VERSION, @@ -7,7 +7,7 @@ import { SdkError, SdkErrorCode, SUPPORTED_PROTOCOL_VERSIONS -} from '@modelcontextprotocol/core'; +} from '@modelcontextprotocol/core-internal'; import { McpServer, Server } from '@modelcontextprotocol/server'; /*** diff --git a/test/integration/test/issues/test1277.zod.v4.description.test.ts b/test/integration/test/issues/test1277.zod.v4.description.test.ts index a8a8d0c7be..0d9c6e9cbe 100644 --- a/test/integration/test/issues/test1277.zod.v4.description.test.ts +++ b/test/integration/test/issues/test1277.zod.v4.description.test.ts @@ -7,7 +7,7 @@ */ import { Client } from '@modelcontextprotocol/client'; -import { InMemoryTransport } from '@modelcontextprotocol/core'; +import { InMemoryTransport } from '@modelcontextprotocol/core-internal'; import { McpServer } from '@modelcontextprotocol/server'; import * as z from 'zod/v4'; diff --git a/test/integration/test/issues/test400.optional-tool-params.test.ts b/test/integration/test/issues/test400.optional-tool-params.test.ts index b71d85b819..7671712734 100644 --- a/test/integration/test/issues/test400.optional-tool-params.test.ts +++ b/test/integration/test/issues/test400.optional-tool-params.test.ts @@ -7,7 +7,7 @@ */ import { Client } from '@modelcontextprotocol/client'; -import { InMemoryTransport } from '@modelcontextprotocol/core'; +import { InMemoryTransport } from '@modelcontextprotocol/core-internal'; import { McpServer } from '@modelcontextprotocol/server'; import * as z from 'zod/v4'; diff --git a/test/integration/test/server.test.ts b/test/integration/test/server.test.ts index dcb9cea87f..51cde29161 100644 --- a/test/integration/test/server.test.ts +++ b/test/integration/test/server.test.ts @@ -9,14 +9,14 @@ import type { jsonSchemaValidator, LoggingMessageNotification, Transport -} from '@modelcontextprotocol/core'; +} from '@modelcontextprotocol/core-internal'; import { InMemoryTransport, LATEST_PROTOCOL_VERSION, SdkError, SdkErrorCode, SUPPORTED_PROTOCOL_VERSIONS -} from '@modelcontextprotocol/core'; +} from '@modelcontextprotocol/core-internal'; import { createMcpExpressApp } from '@modelcontextprotocol/express'; import { McpServer, Server } from '@modelcontextprotocol/server'; import type { Request, Response } from 'express'; diff --git a/test/integration/test/server/declaredCapabilities.test.ts b/test/integration/test/server/declaredCapabilities.test.ts index 60e3f847bb..60e214d129 100644 --- a/test/integration/test/server/declaredCapabilities.test.ts +++ b/test/integration/test/server/declaredCapabilities.test.ts @@ -1,5 +1,5 @@ import { Client } from '@modelcontextprotocol/client'; -import { InMemoryTransport, ProtocolErrorCode } from '@modelcontextprotocol/core'; +import { InMemoryTransport, ProtocolErrorCode } from '@modelcontextprotocol/core-internal'; import { McpServer } from '@modelcontextprotocol/server'; import { describe, expect, test } from 'vitest'; import * as z from 'zod/v4'; diff --git a/test/integration/test/server/elicitation.test.ts b/test/integration/test/server/elicitation.test.ts index 84bb071f1b..5963229f0a 100644 --- a/test/integration/test/server/elicitation.test.ts +++ b/test/integration/test/server/elicitation.test.ts @@ -8,10 +8,10 @@ */ import { Client } from '@modelcontextprotocol/client'; -import type { ElicitRequestFormParams } from '@modelcontextprotocol/core'; -import { InMemoryTransport } from '@modelcontextprotocol/core'; -import { AjvJsonSchemaValidator } from '@modelcontextprotocol/core/validators/ajv'; -import { CfWorkerJsonSchemaValidator } from '@modelcontextprotocol/core/validators/cfWorker'; +import type { ElicitRequestFormParams } from '@modelcontextprotocol/core-internal'; +import { InMemoryTransport } from '@modelcontextprotocol/core-internal'; +import { AjvJsonSchemaValidator } from '@modelcontextprotocol/core-internal/validators/ajv'; +import { CfWorkerJsonSchemaValidator } from '@modelcontextprotocol/core-internal/validators/cfWorker'; import { Server } from '@modelcontextprotocol/server'; const ajvProvider = new AjvJsonSchemaValidator(); diff --git a/test/integration/test/server/mcp.test.ts b/test/integration/test/server/mcp.test.ts index 8c844b11cb..7bc22f2652 100644 --- a/test/integration/test/server/mcp.test.ts +++ b/test/integration/test/server/mcp.test.ts @@ -1,6 +1,12 @@ import { Client } from '@modelcontextprotocol/client'; -import type { Notification, TextContent } from '@modelcontextprotocol/core'; -import { getDisplayName, InMemoryTransport, ProtocolErrorCode, UriTemplate, UrlElicitationRequiredError } from '@modelcontextprotocol/core'; +import type { Notification, TextContent } from '@modelcontextprotocol/core-internal'; +import { + getDisplayName, + InMemoryTransport, + ProtocolErrorCode, + UriTemplate, + UrlElicitationRequiredError +} from '@modelcontextprotocol/core-internal'; import { completable, McpServer, ResourceTemplate } from '@modelcontextprotocol/server'; import { afterEach, beforeEach, describe, expect, test } from 'vitest'; import * as z from 'zod/v4'; diff --git a/test/integration/test/standardSchema.test.ts b/test/integration/test/standardSchema.test.ts index ffc41ce4d8..92f5a6c78c 100644 --- a/test/integration/test/standardSchema.test.ts +++ b/test/integration/test/standardSchema.test.ts @@ -4,8 +4,8 @@ */ import { Client } from '@modelcontextprotocol/client'; -import type { TextContent } from '@modelcontextprotocol/core'; -import { InMemoryTransport } from '@modelcontextprotocol/core'; +import type { TextContent } from '@modelcontextprotocol/core-internal'; +import { InMemoryTransport } from '@modelcontextprotocol/core-internal'; import { completable, fromJsonSchema as serverFromJsonSchema, McpServer } from '@modelcontextprotocol/server'; import { toStandardJsonSchema } from '@valibot/to-json-schema'; import { type } from 'arktype'; diff --git a/test/integration/test/title.test.ts b/test/integration/test/title.test.ts index 588d300832..50e9460dce 100644 --- a/test/integration/test/title.test.ts +++ b/test/integration/test/title.test.ts @@ -1,5 +1,5 @@ import { Client } from '@modelcontextprotocol/client'; -import { InMemoryTransport } from '@modelcontextprotocol/core'; +import { InMemoryTransport } from '@modelcontextprotocol/core-internal'; import { McpServer, ResourceTemplate, Server } from '@modelcontextprotocol/server'; import * as z from 'zod/v4'; diff --git a/test/integration/tsconfig.json b/test/integration/tsconfig.json index 64391c93e0..4764fcfc84 100644 --- a/test/integration/tsconfig.json +++ b/test/integration/tsconfig.json @@ -5,11 +5,15 @@ "compilerOptions": { "paths": { "*": ["./*"], - "@modelcontextprotocol/core": ["./node_modules/@modelcontextprotocol/core/src/index.ts"], - "@modelcontextprotocol/core/public": ["./node_modules/@modelcontextprotocol/core/src/exports/public/index.ts"], - "@modelcontextprotocol/core/validators/ajv": ["./node_modules/@modelcontextprotocol/core/src/validators/ajvProvider.ts"], - "@modelcontextprotocol/core/validators/cfWorker": [ - "./node_modules/@modelcontextprotocol/core/src/validators/cfWorkerProvider.ts" + "@modelcontextprotocol/core-internal": ["./node_modules/@modelcontextprotocol/core-internal/src/index.ts"], + "@modelcontextprotocol/core-internal/public": [ + "./node_modules/@modelcontextprotocol/core-internal/src/exports/public/index.ts" + ], + "@modelcontextprotocol/core-internal/validators/ajv": [ + "./node_modules/@modelcontextprotocol/core-internal/src/validators/ajvProvider.ts" + ], + "@modelcontextprotocol/core-internal/validators/cfWorker": [ + "./node_modules/@modelcontextprotocol/core-internal/src/validators/cfWorkerProvider.ts" ], "@modelcontextprotocol/client": ["./node_modules/@modelcontextprotocol/client/src/index.ts"], "@modelcontextprotocol/client/stdio": ["./node_modules/@modelcontextprotocol/client/src/stdio.ts"], diff --git a/typedoc.config.mjs b/typedoc.config.mjs index ee7bcc663d..4e3ee285fe 100644 --- a/typedoc.config.mjs +++ b/typedoc.config.mjs @@ -48,7 +48,7 @@ export default { }, customJs: 'docs/v2-banner.js', // The spec-generated schema/type JSDoc uses `{@linkcode | method}` cross-references. - // With the data model split across packages (Zod schemas in @modelcontextprotocol/sdk-shared, + // With the data model split across packages (Zod schemas in @modelcontextprotocol/core, // their types in @modelcontextprotocol/server / -client), typedoc's per-package link resolution // can't resolve those bare cross-package references. Disable only the invalid-link check; every // other validation (notExported, etc.) stays on under treatWarningsAsErrors. @@ -58,7 +58,7 @@ export default { treatWarningsAsErrors: true, out: 'tmp/docs/', externalSymbolLinkMappings: { - '@modelcontextprotocol/core': { + '@modelcontextprotocol/core-internal': { StandardSchemaV1: 'https://standardschema.dev/', StandardJSONSchemaV1: 'https://standardschema.dev/' } From 3c69fe86c388a3978da42a3c737eaa73e42521b2 Mon Sep 17 00:00:00 2001 From: Konstantin Konstantinov Date: Thu, 25 Jun 2026 17:13:17 +0300 Subject: [PATCH 16/21] core -> core-internal, sdk-shared -> core --- packages/core-internal/CHANGELOG.md | 107 + packages/core-internal/eslint.config.mjs | 5 + packages/core-internal/package.json | 100 + packages/core-internal/src/auth/errors.ts | 132 + .../src/errors/sdkErrors.examples.ts | 39 + .../core-internal/src/errors/sdkErrors.ts | 110 + .../core-internal/src/exports/public/index.ts | 126 + .../core-internal/src/exports/types/index.ts | 1 + packages/core-internal/src/index.ts | 22 + packages/core-internal/src/shared/auth.ts | 252 ++ .../core-internal/src/shared/authUtils.ts | 57 + .../core-internal/src/shared/metadataUtils.ts | 26 + .../src/shared/protocol.examples.ts | 29 + packages/core-internal/src/shared/protocol.ts | 1066 ++++++ packages/core-internal/src/shared/stdio.ts | 62 + .../src/shared/toolNameValidation.ts | 116 + .../core-internal/src/shared/transport.ts | 134 + .../core-internal/src/shared/uriTemplate.ts | 290 ++ packages/core-internal/src/types/constants.ts | 86 + packages/core-internal/src/types/enums.ts | 26 + packages/core-internal/src/types/errors.ts | 85 + packages/core-internal/src/types/guards.ts | 110 + packages/core-internal/src/types/index.ts | 9 + packages/core-internal/src/types/schemas.ts | 2346 +++++++++++++ .../src/types/spec.types.2025-11-25.ts | 2559 ++++++++++++++ .../src/types/spec.types.2026-07-28.ts | 3030 +++++++++++++++++ .../src/types/specTypeSchema.examples.ts | 40 + .../core-internal/src/types/specTypeSchema.ts | 301 ++ packages/core-internal/src/types/types.ts | 604 ++++ packages/core-internal/src/util/inMemory.ts | 73 + packages/core-internal/src/util/schema.ts | 32 + .../core-internal/src/util/standardSchema.ts | 251 ++ packages/core-internal/src/util/zodCompat.ts | 80 + .../src/validators/ajvProvider.examples.ts | 46 + .../src/validators/ajvProvider.ts | 99 + .../validators/cfWorkerProvider.examples.ts | 33 + .../src/validators/cfWorkerProvider.ts | 81 + .../src/validators/fromJsonSchema.examples.ts | 30 + .../src/validators/fromJsonSchema.ts | 43 + .../src/validators/types.examples.ts | 31 + .../core-internal/src/validators/types.ts | 59 + .../test/errors/sdkHttpError.test.ts | 55 + packages/core-internal/test/inMemory.test.ts | 165 + .../core-internal/test/shared/auth.test.ts | 122 + .../test/shared/authUtils.test.ts | 90 + .../test/shared/customMethods.test.ts | 198 ++ .../test/shared/protocol.test.ts | 912 +++++ .../shared/protocolTransportHandling.test.ts | 123 + .../core-internal/test/shared/stdio.test.ts | 158 + .../test/shared/toolNameValidation.test.ts | 130 + .../test/shared/traceContextMeta.test.ts | 120 + .../test/shared/transport.test.ts | 182 + .../test/shared/uriTemplate.test.ts | 314 ++ .../test/shared/wrapHandler.test.ts | 33 + .../test/spec.types.2025-11-25.test.ts | 950 ++++++ .../test/spec.types.2026-07-28.test.ts | 550 +++ .../test/types.capabilities.test.ts | 103 + packages/core-internal/test/types.test.ts | 1174 +++++++ .../core-internal/test/types/errors.test.ts | 44 + .../core-internal/test/types/guards.test.ts | 123 + .../test/types/specTypeSchema.test.ts | 177 + .../test/util/standardSchema.test.ts | 42 + .../util/standardSchema.zodFallback.test.ts | 37 + .../core-internal/test/util/zodCompat.test.ts | 89 + .../test/validators/validators.test.ts | 625 ++++ packages/core-internal/tsconfig.json | 12 + packages/core-internal/vitest.config.js | 3 + 67 files changed, 19259 insertions(+) create mode 100644 packages/core-internal/CHANGELOG.md create mode 100644 packages/core-internal/eslint.config.mjs create mode 100644 packages/core-internal/package.json create mode 100644 packages/core-internal/src/auth/errors.ts create mode 100644 packages/core-internal/src/errors/sdkErrors.examples.ts create mode 100644 packages/core-internal/src/errors/sdkErrors.ts create mode 100644 packages/core-internal/src/exports/public/index.ts create mode 100644 packages/core-internal/src/exports/types/index.ts create mode 100644 packages/core-internal/src/index.ts create mode 100644 packages/core-internal/src/shared/auth.ts create mode 100644 packages/core-internal/src/shared/authUtils.ts create mode 100644 packages/core-internal/src/shared/metadataUtils.ts create mode 100644 packages/core-internal/src/shared/protocol.examples.ts create mode 100644 packages/core-internal/src/shared/protocol.ts create mode 100644 packages/core-internal/src/shared/stdio.ts create mode 100644 packages/core-internal/src/shared/toolNameValidation.ts create mode 100644 packages/core-internal/src/shared/transport.ts create mode 100644 packages/core-internal/src/shared/uriTemplate.ts create mode 100644 packages/core-internal/src/types/constants.ts create mode 100644 packages/core-internal/src/types/enums.ts create mode 100644 packages/core-internal/src/types/errors.ts create mode 100644 packages/core-internal/src/types/guards.ts create mode 100644 packages/core-internal/src/types/index.ts create mode 100644 packages/core-internal/src/types/schemas.ts create mode 100644 packages/core-internal/src/types/spec.types.2025-11-25.ts create mode 100644 packages/core-internal/src/types/spec.types.2026-07-28.ts create mode 100644 packages/core-internal/src/types/specTypeSchema.examples.ts create mode 100644 packages/core-internal/src/types/specTypeSchema.ts create mode 100644 packages/core-internal/src/types/types.ts create mode 100644 packages/core-internal/src/util/inMemory.ts create mode 100644 packages/core-internal/src/util/schema.ts create mode 100644 packages/core-internal/src/util/standardSchema.ts create mode 100644 packages/core-internal/src/util/zodCompat.ts create mode 100644 packages/core-internal/src/validators/ajvProvider.examples.ts create mode 100644 packages/core-internal/src/validators/ajvProvider.ts create mode 100644 packages/core-internal/src/validators/cfWorkerProvider.examples.ts create mode 100644 packages/core-internal/src/validators/cfWorkerProvider.ts create mode 100644 packages/core-internal/src/validators/fromJsonSchema.examples.ts create mode 100644 packages/core-internal/src/validators/fromJsonSchema.ts create mode 100644 packages/core-internal/src/validators/types.examples.ts create mode 100644 packages/core-internal/src/validators/types.ts create mode 100644 packages/core-internal/test/errors/sdkHttpError.test.ts create mode 100644 packages/core-internal/test/inMemory.test.ts create mode 100644 packages/core-internal/test/shared/auth.test.ts create mode 100644 packages/core-internal/test/shared/authUtils.test.ts create mode 100644 packages/core-internal/test/shared/customMethods.test.ts create mode 100644 packages/core-internal/test/shared/protocol.test.ts create mode 100644 packages/core-internal/test/shared/protocolTransportHandling.test.ts create mode 100644 packages/core-internal/test/shared/stdio.test.ts create mode 100644 packages/core-internal/test/shared/toolNameValidation.test.ts create mode 100644 packages/core-internal/test/shared/traceContextMeta.test.ts create mode 100644 packages/core-internal/test/shared/transport.test.ts create mode 100644 packages/core-internal/test/shared/uriTemplate.test.ts create mode 100644 packages/core-internal/test/shared/wrapHandler.test.ts create mode 100644 packages/core-internal/test/spec.types.2025-11-25.test.ts create mode 100644 packages/core-internal/test/spec.types.2026-07-28.test.ts create mode 100644 packages/core-internal/test/types.capabilities.test.ts create mode 100644 packages/core-internal/test/types.test.ts create mode 100644 packages/core-internal/test/types/errors.test.ts create mode 100644 packages/core-internal/test/types/guards.test.ts create mode 100644 packages/core-internal/test/types/specTypeSchema.test.ts create mode 100644 packages/core-internal/test/util/standardSchema.test.ts create mode 100644 packages/core-internal/test/util/standardSchema.zodFallback.test.ts create mode 100644 packages/core-internal/test/util/zodCompat.test.ts create mode 100644 packages/core-internal/test/validators/validators.test.ts create mode 100644 packages/core-internal/tsconfig.json create mode 100644 packages/core-internal/vitest.config.js diff --git a/packages/core-internal/CHANGELOG.md b/packages/core-internal/CHANGELOG.md new file mode 100644 index 0000000000..767fb292c2 --- /dev/null +++ b/packages/core-internal/CHANGELOG.md @@ -0,0 +1,107 @@ +# @modelcontextprotocol/core-internal + +## 2.0.0-alpha.1 + +### Minor Changes + +- [#1673](https://github.com/modelcontextprotocol/typescript-sdk/pull/1673) [`462c3fc`](https://github.com/modelcontextprotocol/typescript-sdk/commit/462c3fc47dffac908d2ba27784d47ff010fa065e) Thanks [@KKonstantinov](https://github.com/KKonstantinov)! - refactor: extract task + orchestration from Protocol into TaskManager + + **Breaking changes:** + - `taskStore`, `taskMessageQueue`, `defaultTaskPollInterval`, and `maxTaskQueueSize` moved from `ProtocolOptions` to `capabilities.tasks` on `ClientOptions`/`ServerOptions` + +- [#1389](https://github.com/modelcontextprotocol/typescript-sdk/pull/1389) [`108f2f3`](https://github.com/modelcontextprotocol/typescript-sdk/commit/108f2f3ab6a1267587c7c4f900b6eca3cc2dae51) Thanks [@DePasqualeOrg](https://github.com/DePasqualeOrg)! - Fix error handling for + unknown tools and resources per MCP spec. + + **Tools:** Unknown or disabled tool calls now return JSON-RPC protocol errors with code `-32602` (InvalidParams) instead of `CallToolResult` with `isError: true`. Callers who checked `result.isError` for unknown tools should catch rejected promises instead. + + **Resources:** Unknown resource reads now return error code `-32002` (ResourceNotFound) instead of `-32602` (InvalidParams). + + Added `ProtocolErrorCode.ResourceNotFound`. + +- [#1689](https://github.com/modelcontextprotocol/typescript-sdk/pull/1689) [`0784be1`](https://github.com/modelcontextprotocol/typescript-sdk/commit/0784be1a67fb3cc2aba0182d88151264f4ea73c8) Thanks [@felixweinberger](https://github.com/felixweinberger)! - Support Standard Schema + for tool and prompt schemas + + Tool and prompt registration now accepts any schema library that implements the [Standard Schema spec](https://standardschema.dev/): Zod v4, Valibot, ArkType, and others. `RegisteredTool.inputSchema`, `RegisteredTool.outputSchema`, and `RegisteredPrompt.argsSchema` now use + `StandardSchemaWithJSON` (requires both `~standard.validate` and `~standard.jsonSchema`) instead of the Zod-specific `AnySchema` type. + + **Zod v4 schemas continue to work unchanged** — Zod v4 implements the required interfaces natively. + + ```typescript + import { type } from 'arktype'; + + server.registerTool( + 'greet', + { + inputSchema: type({ name: 'string' }) + }, + async ({ name }) => ({ content: [{ type: 'text', text: `Hello, ${name}!` }] }) + ); + ``` + + For raw JSON Schema (e.g. TypeBox output), use the new `fromJsonSchema` adapter: + + ```typescript + import { fromJsonSchema, AjvJsonSchemaValidator } from '@modelcontextprotocol/core-internal'; + + server.registerTool( + 'greet', + { + inputSchema: fromJsonSchema({ type: 'object', properties: { name: { type: 'string' } } }, new AjvJsonSchemaValidator()) + }, + handler + ); + ``` + + **Breaking changes:** + - `experimental.tasks.getTaskResult()` no longer accepts a `resultSchema` parameter. Returns `GetTaskPayloadResult` (a loose `Result`); cast to the expected type at the call site. + - Removed unused exports from `@modelcontextprotocol/core-internal`: `SchemaInput`, `schemaToJson`, `parseSchemaAsync`, `getSchemaShape`, `getSchemaDescription`, `isOptionalSchema`, `unwrapOptionalSchema`. Use the new `standardSchemaToJsonSchema` and `validateStandardSchema` + instead. + - `completable()` remains Zod-specific (it relies on Zod's `.shape` introspection). + +### Patch Changes + +- [#1735](https://github.com/modelcontextprotocol/typescript-sdk/pull/1735) [`a2e5037`](https://github.com/modelcontextprotocol/typescript-sdk/commit/a2e503733f6f3eea3a79a80bdc1b3cdd743f8bb3) Thanks [@felixweinberger](https://github.com/felixweinberger)! - Abort in-flight request + handlers when the connection closes. Previously, request handlers would continue running after the transport disconnected, wasting resources and preventing proper cleanup. Also fixes `InMemoryTransport.close()` firing `onclose` twice on the initiating side. + +- [#1574](https://github.com/modelcontextprotocol/typescript-sdk/pull/1574) [`379392d`](https://github.com/modelcontextprotocol/typescript-sdk/commit/379392d04460ee2cbeecae374901fae21e525031) Thanks [@olaservo](https://github.com/olaservo)! - Add missing `size` field to + `ResourceSchema` to match the MCP specification + +- [#1363](https://github.com/modelcontextprotocol/typescript-sdk/pull/1363) [`0a75810`](https://github.com/modelcontextprotocol/typescript-sdk/commit/0a75810b26e24bae6b9cfb41e12ac770aeaa1da4) Thanks [@DevJanderson](https://github.com/DevJanderson)! - Fix ReDoS vulnerability in + UriTemplate regex patterns (CVE-2026-0621) + +- [#1761](https://github.com/modelcontextprotocol/typescript-sdk/pull/1761) [`01954e6`](https://github.com/modelcontextprotocol/typescript-sdk/commit/01954e621afe525cc3c1bbe8d781e44734cf81c2) Thanks [@felixweinberger](https://github.com/felixweinberger)! - Convert remaining + capability-assertion throws to `SdkError(SdkErrorCode.CapabilityNotSupported, ...)`. Follow-up to #1454 which missed `Client.assertCapability()`, the task capability helpers in `experimental/tasks/helpers.ts`, and the sampling/elicitation capability checks in + `experimental/tasks/server.ts`. + +- [#1790](https://github.com/modelcontextprotocol/typescript-sdk/pull/1790) [`89fb094`](https://github.com/modelcontextprotocol/typescript-sdk/commit/89fb0947b487b37f9bfcc2a2486dcd33d3922f8e) Thanks [@felixweinberger](https://github.com/felixweinberger)! - Consolidate per-request + cleanup in `_requestWithSchema` into a single `.finally()` block. This fixes an abort signal listener leak (listeners accumulated when a caller reused one `AbortSignal` across requests) and two cases where `_responseHandlers` entries leaked on send-failure paths. + +- [#1486](https://github.com/modelcontextprotocol/typescript-sdk/pull/1486) [`65bbcea`](https://github.com/modelcontextprotocol/typescript-sdk/commit/65bbceab773277f056a9d3e385e7e7d8cef54f9b) Thanks [@localden](https://github.com/localden)! - Fix InMemoryTaskStore to enforce + session isolation. Previously, sessionId was accepted but ignored on all TaskStore methods, allowing any session to enumerate, read, and mutate tasks created by other sessions. The store now persists sessionId at creation time and enforces ownership on all reads and writes. + +- [#1766](https://github.com/modelcontextprotocol/typescript-sdk/pull/1766) [`48aba0d`](https://github.com/modelcontextprotocol/typescript-sdk/commit/48aba0d3c3b2ee04c442934095b663d19e07a3b3) Thanks [@felixweinberger](https://github.com/felixweinberger)! - Add explicit + `| undefined` to optional properties on the `Transport` interface and `TransportSendOptions` (`onclose`, `onerror`, `onmessage`, `sessionId`, `setProtocolVersion`, `setSupportedProtocolVersions`, `onresumptiontoken`). + + This fixes TS2420 errors for consumers using `exactOptionalPropertyTypes: true` without `skipLibCheck`, where the emitted `.d.ts` for implementing classes included `| undefined` but the interface did not. + + Workaround for older SDK versions: enable `skipLibCheck: true` in your tsconfig. + +- [#1419](https://github.com/modelcontextprotocol/typescript-sdk/pull/1419) [`dcf708d`](https://github.com/modelcontextprotocol/typescript-sdk/commit/dcf708d892b7ca5f137c74109d42cdeb05e2ee3a) Thanks [@KKonstantinov](https://github.com/KKonstantinov)! - remove deprecated .tool, + .prompt, .resource method signatures + +- [#1534](https://github.com/modelcontextprotocol/typescript-sdk/pull/1534) [`69a0626`](https://github.com/modelcontextprotocol/typescript-sdk/commit/69a062693f61e024d7a366db0c3e3ba74ff59d8e) Thanks [@josefaidt](https://github.com/josefaidt)! - remove npm references, use pnpm + +- [#1534](https://github.com/modelcontextprotocol/typescript-sdk/pull/1534) [`69a0626`](https://github.com/modelcontextprotocol/typescript-sdk/commit/69a062693f61e024d7a366db0c3e3ba74ff59d8e) Thanks [@josefaidt](https://github.com/josefaidt)! - clean up package manager usage, all + pnpm + +- [#1796](https://github.com/modelcontextprotocol/typescript-sdk/pull/1796) [`d6a02c8`](https://github.com/modelcontextprotocol/typescript-sdk/commit/d6a02c85c0514658c27615398a3003aadce80fb0) Thanks [@felixweinberger](https://github.com/felixweinberger)! - Ensure + `standardSchemaToJsonSchema` emits `type: "object"` at the root, fixing discriminated-union tool/prompt schemas that previously produced `{oneOf: [...]}` without the MCP-required top-level type. Also throws a clear error when given an explicitly non-object schema (e.g. + `z.string()`). Fixes #1643. + +- [#1419](https://github.com/modelcontextprotocol/typescript-sdk/pull/1419) [`dcf708d`](https://github.com/modelcontextprotocol/typescript-sdk/commit/dcf708d892b7ca5f137c74109d42cdeb05e2ee3a) Thanks [@KKonstantinov](https://github.com/KKonstantinov)! - deprecated .tool, .prompt, + .resource method removal + +- [#1762](https://github.com/modelcontextprotocol/typescript-sdk/pull/1762) [`64897f7`](https://github.com/modelcontextprotocol/typescript-sdk/commit/64897f78ce78f736b027dfecd1b4326c8c6678c7) Thanks [@felixweinberger](https://github.com/felixweinberger)! - + `ReadBuffer.readMessage()` now silently skips non-JSON lines instead of throwing `SyntaxError`. This prevents noisy `onerror` callbacks when hot-reload tools (tsx, nodemon) write debug output like "Gracefully restarting..." to stdout. Lines that parse as JSON but fail JSONRPC + schema validation still throw. diff --git a/packages/core-internal/eslint.config.mjs b/packages/core-internal/eslint.config.mjs new file mode 100644 index 0000000000..951c9f3a91 --- /dev/null +++ b/packages/core-internal/eslint.config.mjs @@ -0,0 +1,5 @@ +// @ts-check + +import baseConfig from '@modelcontextprotocol/eslint-config'; + +export default baseConfig; diff --git a/packages/core-internal/package.json b/packages/core-internal/package.json new file mode 100644 index 0000000000..5185a67def --- /dev/null +++ b/packages/core-internal/package.json @@ -0,0 +1,100 @@ +{ + "name": "@modelcontextprotocol/core-internal", + "private": true, + "version": "2.0.0-alpha.1", + "description": "Model Context Protocol implementation for TypeScript - Core package", + "license": "MIT", + "author": "Anthropic, PBC (https://anthropic.com)", + "homepage": "https://modelcontextprotocol.io", + "bugs": "https://github.com/modelcontextprotocol/typescript-sdk/issues", + "type": "module", + "repository": { + "type": "git", + "url": "git+https://github.com/modelcontextprotocol/typescript-sdk.git" + }, + "engines": { + "node": ">=20" + }, + "keywords": [ + "modelcontextprotocol", + "mcp", + "core" + ], + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.mjs" + }, + "./types": { + "types": "./src/exports/types/index.ts", + "import": "./src/exports/types/index.ts" + }, + "./public": { + "types": "./src/exports/public/index.ts", + "import": "./src/exports/public/index.ts" + }, + "./validators/ajv": { + "types": "./src/validators/ajvProvider.ts", + "import": "./src/validators/ajvProvider.ts" + }, + "./validators/cfWorker": { + "types": "./src/validators/cfWorkerProvider.ts", + "import": "./src/validators/cfWorkerProvider.ts" + } + }, + "scripts": { + "typecheck": "tsgo -p tsconfig.json --noEmit", + "lint": "eslint src/ && prettier --ignore-path ../../.prettierignore --check .", + "lint:fix": "eslint src/ --fix && prettier --ignore-path ../../.prettierignore --write .", + "check": "pnpm run typecheck && pnpm run lint", + "test": "vitest run", + "test:watch": "vitest" + }, + "dependencies": { + "json-schema-typed": "catalog:runtimeShared", + "zod": "catalog:runtimeShared" + }, + "peerDependencies": { + "@cfworker/json-schema": "catalog:runtimeShared", + "ajv": "catalog:runtimeShared", + "ajv-formats": "catalog:runtimeShared", + "zod": "catalog:runtimeShared" + }, + "peerDependenciesMeta": { + "@cfworker/json-schema": { + "optional": true + }, + "ajv": { + "optional": true + }, + "ajv-formats": { + "optional": true + }, + "zod": { + "optional": false + } + }, + "devDependencies": { + "@modelcontextprotocol/tsconfig": "workspace:^", + "@modelcontextprotocol/vitest-config": "workspace:^", + "@modelcontextprotocol/eslint-config": "workspace:^", + "@cfworker/json-schema": "catalog:runtimeShared", + "ajv": "catalog:runtimeShared", + "ajv-formats": "catalog:runtimeShared", + "@eslint/js": "catalog:devTools", + "@types/content-type": "catalog:devTools", + "@types/cors": "catalog:devTools", + "@types/cross-spawn": "catalog:devTools", + "@types/eventsource": "catalog:devTools", + "@types/express": "catalog:devTools", + "@types/express-serve-static-core": "catalog:devTools", + "@typescript/native-preview": "catalog:devTools", + "eslint": "catalog:devTools", + "eslint-config-prettier": "catalog:devTools", + "eslint-plugin-n": "catalog:devTools", + "prettier": "catalog:devTools", + "typescript": "catalog:devTools", + "typescript-eslint": "catalog:devTools", + "vitest": "catalog:devTools" + } +} diff --git a/packages/core-internal/src/auth/errors.ts b/packages/core-internal/src/auth/errors.ts new file mode 100644 index 0000000000..f2060887f1 --- /dev/null +++ b/packages/core-internal/src/auth/errors.ts @@ -0,0 +1,132 @@ +import type { OAuthErrorResponse } from '../shared/auth'; + +/** + * OAuth error codes as defined by {@link https://datatracker.ietf.org/doc/html/rfc6749#section-5.2 | RFC 6749} + * and extensions. + */ +export enum OAuthErrorCode { + /** + * The request is missing a required parameter, includes an invalid parameter value, + * includes a parameter more than once, or is otherwise malformed. + */ + InvalidRequest = 'invalid_request', + + /** + * Client authentication failed (e.g., unknown client, no client authentication included, + * or unsupported authentication method). + */ + InvalidClient = 'invalid_client', + + /** + * The provided authorization grant or refresh token is invalid, expired, revoked, + * does not match the redirection URI used in the authorization request, or was issued to another client. + */ + InvalidGrant = 'invalid_grant', + + /** + * The authenticated client is not authorized to use this authorization grant type. + */ + UnauthorizedClient = 'unauthorized_client', + + /** + * The authorization grant type is not supported by the authorization server. + */ + UnsupportedGrantType = 'unsupported_grant_type', + + /** + * The requested scope is invalid, unknown, malformed, or exceeds the scope granted by the resource owner. + */ + InvalidScope = 'invalid_scope', + + /** + * The resource owner or authorization server denied the request. + */ + AccessDenied = 'access_denied', + + /** + * The authorization server encountered an unexpected condition that prevented it from fulfilling the request. + */ + ServerError = 'server_error', + + /** + * The authorization server is currently unable to handle the request due to temporary overloading or maintenance. + */ + TemporarilyUnavailable = 'temporarily_unavailable', + + /** + * The authorization server does not support obtaining an authorization code using this method. + */ + UnsupportedResponseType = 'unsupported_response_type', + + /** + * The authorization server does not support the requested token type. + */ + UnsupportedTokenType = 'unsupported_token_type', + + /** + * The access token provided is expired, revoked, malformed, or invalid for other reasons. + */ + InvalidToken = 'invalid_token', + + /** + * The HTTP method used is not allowed for this endpoint. (Custom, non-standard error) + */ + MethodNotAllowed = 'method_not_allowed', + + /** + * Rate limit exceeded. (Custom, non-standard error based on RFC 6585) + */ + TooManyRequests = 'too_many_requests', + + /** + * The client metadata is invalid. (Custom error for dynamic client registration - RFC 7591) + */ + InvalidClientMetadata = 'invalid_client_metadata', + + /** + * The request requires higher privileges than provided by the access token. + */ + InsufficientScope = 'insufficient_scope', + + /** + * The requested resource is invalid, missing, unknown, or malformed. (Custom error for resource indicators - RFC 8707) + */ + InvalidTarget = 'invalid_target' +} + +/** + * OAuth error class for all OAuth-related errors. + */ +export class OAuthError extends Error { + constructor( + public readonly code: OAuthErrorCode | string, + message: string, + public readonly errorUri?: string + ) { + super(message); + this.name = 'OAuthError'; + } + + /** + * Converts the error to a standard OAuth error response object. + */ + toResponseObject(): OAuthErrorResponse { + const response: OAuthErrorResponse = { + error: this.code, + error_description: this.message + }; + + if (this.errorUri) { + response.error_uri = this.errorUri; + } + + return response; + } + + /** + * Creates an {@linkcode OAuthError} from an OAuth error response. + */ + static fromResponse(response: OAuthErrorResponse): OAuthError { + return new OAuthError(response.error as OAuthErrorCode, response.error_description ?? response.error, response.error_uri); + } +} diff --git a/packages/core-internal/src/errors/sdkErrors.examples.ts b/packages/core-internal/src/errors/sdkErrors.examples.ts new file mode 100644 index 0000000000..c828d443ec --- /dev/null +++ b/packages/core-internal/src/errors/sdkErrors.examples.ts @@ -0,0 +1,39 @@ +/** + * Type-checked examples for `sdkErrors.ts`. + * + * These examples are synced into JSDoc comments via the sync-snippets script. + * Each function's region markers define the code snippet that appears in the docs. + * + * @module + */ + +import { SdkError, SdkErrorCode, SdkHttpError } from './sdkErrors'; + +/** + * Example: Throwing and catching SDK errors. + */ +function SdkError_basicUsage() { + //#region SdkError_basicUsage + try { + // Throwing an SDK error + throw new SdkError(SdkErrorCode.NotConnected, 'Transport is not connected'); + } catch (error) { + // Checking error type by code + if (error instanceof SdkError && error.code === SdkErrorCode.RequestTimeout) { + // Handle timeout + } + } + //#endregion SdkError_basicUsage +} + +/** + * Example: Checking for HTTP transport errors. + */ +function SdkHttpError_basicUsage(error: unknown) { + //#region SdkHttpError_basicUsage + if (error instanceof SdkHttpError) { + console.log(error.status); // number + console.log(error.statusText); // string | undefined + } + //#endregion SdkHttpError_basicUsage +} diff --git a/packages/core-internal/src/errors/sdkErrors.ts b/packages/core-internal/src/errors/sdkErrors.ts new file mode 100644 index 0000000000..af432c6389 --- /dev/null +++ b/packages/core-internal/src/errors/sdkErrors.ts @@ -0,0 +1,110 @@ +/** + * Error codes for SDK errors (local errors that never cross the wire). + * Unlike {@linkcode ProtocolErrorCode} which uses numeric JSON-RPC codes, `SdkErrorCode` uses + * descriptive string values for better developer experience. + * + * These errors are thrown locally by the SDK and are never serialized as + * JSON-RPC error responses. + */ +export enum SdkErrorCode { + // State errors + /** Transport is not connected */ + NotConnected = 'NOT_CONNECTED', + /** Transport is already connected */ + AlreadyConnected = 'ALREADY_CONNECTED', + /** Protocol is not initialized */ + NotInitialized = 'NOT_INITIALIZED', + + // Capability errors + /** Required capability is not supported by the remote side */ + CapabilityNotSupported = 'CAPABILITY_NOT_SUPPORTED', + + // Transport errors + /** Request timed out waiting for response */ + RequestTimeout = 'REQUEST_TIMEOUT', + /** Connection was closed */ + ConnectionClosed = 'CONNECTION_CLOSED', + /** Failed to send message */ + SendFailed = 'SEND_FAILED', + /** Response result failed local schema validation */ + InvalidResult = 'INVALID_RESULT', + + // Transport errors + ClientHttpNotImplemented = 'CLIENT_HTTP_NOT_IMPLEMENTED', + ClientHttpAuthentication = 'CLIENT_HTTP_AUTHENTICATION', + ClientHttpForbidden = 'CLIENT_HTTP_FORBIDDEN', + ClientHttpUnexpectedContent = 'CLIENT_HTTP_UNEXPECTED_CONTENT', + ClientHttpFailedToOpenStream = 'CLIENT_HTTP_FAILED_TO_OPEN_STREAM', + ClientHttpFailedToTerminateSession = 'CLIENT_HTTP_FAILED_TO_TERMINATE_SESSION' +} + +/** + * SDK errors are local errors that never cross the wire. + * They are distinct from {@linkcode ProtocolError} which represents JSON-RPC protocol errors + * that are serialized and sent as error responses. + * + * @example + * ```ts source="./sdkErrors.examples.ts#SdkError_basicUsage" + * try { + * // Throwing an SDK error + * throw new SdkError(SdkErrorCode.NotConnected, 'Transport is not connected'); + * } catch (error) { + * // Checking error type by code + * if (error instanceof SdkError && error.code === SdkErrorCode.RequestTimeout) { + * // Handle timeout + * } + * } + * ``` + */ +export class SdkError extends Error { + constructor( + public readonly code: SdkErrorCode, + message: string, + public readonly data?: unknown + ) { + super(message); + this.name = 'SdkError'; + } +} + +/** + * Typed shape for HTTP error data carried by {@linkcode SdkHttpError}. + */ +export interface SdkHttpErrorData { + status: number; + statusText?: string; + [key: string]: unknown; +} + +/** + * An {@linkcode SdkError} subclass for HTTP transport failures. + * + * Thrown by the streamable HTTP transport when the server responds with a + * non-OK status code. Narrows {@linkcode SdkError.data | data} to + * {@linkcode SdkHttpErrorData} so consumers can inspect the HTTP status + * without unsafe casting. + * + * @example + * ```ts source="./sdkErrors.examples.ts#SdkHttpError_basicUsage" + * if (error instanceof SdkHttpError) { + * console.log(error.status); // number + * console.log(error.statusText); // string | undefined + * } + * ``` + */ +export class SdkHttpError extends SdkError { + declare readonly data: SdkHttpErrorData; + + constructor(code: SdkErrorCode, message: string, data: SdkHttpErrorData) { + super(code, message, data); + this.name = 'SdkHttpError'; + } + + get status(): number { + return this.data.status; + } + + get statusText(): string | undefined { + return this.data.statusText; + } +} diff --git a/packages/core-internal/src/exports/public/index.ts b/packages/core-internal/src/exports/public/index.ts new file mode 100644 index 0000000000..88d4942a74 --- /dev/null +++ b/packages/core-internal/src/exports/public/index.ts @@ -0,0 +1,126 @@ +/** + * Curated public API exports for @modelcontextprotocol/core-internal. + * + * This module defines the stable, public-facing API surface. Client and server + * packages re-export from here so that end users only see supported symbols. + * + * Internal utilities (Protocol class, stdio parsing, schema helpers, etc.) + * remain available via the internal barrel (@modelcontextprotocol/core-internal) for + * use by client/server packages. + */ + +// Auth error classes +export { OAuthError, OAuthErrorCode } from '../../auth/errors'; + +// SDK error types (local errors that never cross the wire) +export type { SdkHttpErrorData } from '../../errors/sdkErrors'; +export { SdkError, SdkErrorCode, SdkHttpError } from '../../errors/sdkErrors'; + +// Auth TypeScript types (NOT Zod schemas like OAuthMetadataSchema) +export type { + AuthorizationServerMetadata, + IdJagTokenExchangeResponse, + OAuthClientInformation, + OAuthClientInformationFull, + OAuthClientInformationMixed, + OAuthClientMetadata, + OAuthClientRegistrationError, + OAuthErrorResponse, + OAuthMetadata, + OAuthProtectedResourceMetadata, + OAuthTokenRevocationRequest, + OAuthTokens, + OpenIdProviderDiscoveryMetadata, + OpenIdProviderMetadata +} from '../../shared/auth'; + +// Auth utilities +export { checkResourceAllowed, resourceUrlFromServerUrl } from '../../shared/authUtils'; + +// Metadata utilities +export { getDisplayName } from '../../shared/metadataUtils'; + +// Protocol types (NOT the Protocol class itself or mergeCapabilities) +export type { + BaseContext, + ClientContext, + NotificationOptions, + ProgressCallback, + ProtocolOptions, + RequestHandlerSchemas, + RequestOptions, + ServerContext +} from '../../shared/protocol'; +export { DEFAULT_REQUEST_TIMEOUT_MSEC } from '../../shared/protocol'; + +// stdio message framing utilities (for custom transport authors) +export { deserializeMessage, ReadBuffer, serializeMessage, STDIO_DEFAULT_MAX_BUFFER_SIZE } from '../../shared/stdio'; + +// Transport types (NOT normalizeHeaders) +export type { FetchLike, Transport, TransportSendOptions } from '../../shared/transport'; +export { createFetchWithInit } from '../../shared/transport'; +export { InMemoryTransport } from '../../util/inMemory'; + +// URI Template +export type { Variables } from '../../shared/uriTemplate'; +export { UriTemplate } from '../../shared/uriTemplate'; + +// Types — all TypeScript types (standalone interfaces + schema-derived). +// This is the one intentional `export *`: types.ts contains only spec-derived TS +// types, and every type there should be public. See comment in types.ts. +export * from '../../types/types'; + +// Constants +export { + BAGGAGE_META_KEY, + CLIENT_CAPABILITIES_META_KEY, + CLIENT_INFO_META_KEY, + DEFAULT_NEGOTIATED_PROTOCOL_VERSION, + INTERNAL_ERROR, + INVALID_PARAMS, + INVALID_REQUEST, + JSONRPC_VERSION, + LATEST_PROTOCOL_VERSION, + LOG_LEVEL_META_KEY, + METHOD_NOT_FOUND, + PARSE_ERROR, + PROTOCOL_VERSION_META_KEY, + RELATED_TASK_META_KEY, + SUPPORTED_PROTOCOL_VERSIONS, + TRACEPARENT_META_KEY, + TRACESTATE_META_KEY +} from '../../types/constants'; + +// Enums +export { ProtocolErrorCode } from '../../types/enums'; + +// Error classes +export { ProtocolError, UnsupportedProtocolVersionError, UrlElicitationRequiredError } from '../../types/errors'; + +// Type guards and message parsing +export { + assertCompleteRequestPrompt, + assertCompleteRequestResourceTemplate, + isCallToolResult, + isInitializedNotification, + isInitializeRequest, + isJSONRPCErrorResponse, + isJSONRPCNotification, + isJSONRPCRequest, + isJSONRPCResponse, + isJSONRPCResultResponse, + isTaskAugmentedRequestParams, + parseJSONRPCMessage +} from '../../types/guards'; + +// Validator types and classes +export type { SpecTypeName, SpecTypes } from '../../types/specTypeSchema'; +export { isSpecType, specTypeSchemas } from '../../types/specTypeSchema'; +export type { StandardSchemaV1, StandardSchemaV1Sync, StandardSchemaWithJSON } from '../../util/standardSchema'; +// Validator providers are type-only here — import the runtime classes from the explicit +// `@modelcontextprotocol/{client,server}/validators/{ajv,cf-worker}` subpaths to customise. +export type { AjvJsonSchemaValidator } from '../../validators/ajvProvider'; +export type { CfWorkerJsonSchemaValidator, CfWorkerSchemaDraft } from '../../validators/cfWorkerProvider'; +// fromJsonSchema is intentionally NOT exported here — the server and client packages +// provide runtime-aware wrappers that default to the appropriate validator via _shims. +export type { JsonSchemaType, JsonSchemaValidator, jsonSchemaValidator, JsonSchemaValidatorResult } from '../../validators/types'; diff --git a/packages/core-internal/src/exports/types/index.ts b/packages/core-internal/src/exports/types/index.ts new file mode 100644 index 0000000000..cfd18a47ce --- /dev/null +++ b/packages/core-internal/src/exports/types/index.ts @@ -0,0 +1 @@ +export type * from '../../types/index'; diff --git a/packages/core-internal/src/index.ts b/packages/core-internal/src/index.ts new file mode 100644 index 0000000000..940ab08187 --- /dev/null +++ b/packages/core-internal/src/index.ts @@ -0,0 +1,22 @@ +export * from './auth/errors'; +export * from './errors/sdkErrors'; +export * from './shared/auth'; +export * from './shared/authUtils'; +export * from './shared/metadataUtils'; +export * from './shared/protocol'; +export * from './shared/stdio'; +export * from './shared/toolNameValidation'; +export * from './shared/transport'; +export * from './shared/uriTemplate'; +export * from './types/index'; +export * from './util/inMemory'; +export * from './util/schema'; +export * from './util/standardSchema'; +export * from './util/zodCompat'; + +// Validator providers are type-only here — import the runtime classes from the explicit +// `@modelcontextprotocol/{core,client,server}/validators/{ajv,cf-worker}` subpaths to customise. +export type { AjvJsonSchemaValidator } from './validators/ajvProvider'; +export type { CfWorkerJsonSchemaValidator, CfWorkerSchemaDraft } from './validators/cfWorkerProvider'; +export * from './validators/fromJsonSchema'; +export type { JsonSchemaType, JsonSchemaValidator, jsonSchemaValidator, JsonSchemaValidatorResult } from './validators/types'; diff --git a/packages/core-internal/src/shared/auth.ts b/packages/core-internal/src/shared/auth.ts new file mode 100644 index 0000000000..deee583aa1 --- /dev/null +++ b/packages/core-internal/src/shared/auth.ts @@ -0,0 +1,252 @@ +import * as z from 'zod/v4'; + +/** + * Reusable URL validation that disallows `javascript:` scheme + */ +export const SafeUrlSchema = z + .url() + .superRefine((val, ctx) => { + if (!URL.canParse(val)) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: 'URL must be parseable', + fatal: true + }); + + return z.NEVER; + } + }) + .refine( + url => { + const u = new URL(url); + return u.protocol !== 'javascript:' && u.protocol !== 'data:' && u.protocol !== 'vbscript:'; + }, + { message: 'URL cannot use javascript:, data:, or vbscript: scheme' } + ); + +/** + * RFC 9728 OAuth Protected Resource Metadata + */ +export const OAuthProtectedResourceMetadataSchema = z.looseObject({ + resource: z.string().url(), + authorization_servers: z.array(SafeUrlSchema).optional(), + jwks_uri: z.string().url().optional(), + scopes_supported: z.array(z.string()).optional(), + bearer_methods_supported: z.array(z.string()).optional(), + resource_signing_alg_values_supported: z.array(z.string()).optional(), + resource_name: z.string().optional(), + resource_documentation: z.string().optional(), + resource_policy_uri: z.string().url().optional(), + resource_tos_uri: z.string().url().optional(), + tls_client_certificate_bound_access_tokens: z.boolean().optional(), + authorization_details_types_supported: z.array(z.string()).optional(), + dpop_signing_alg_values_supported: z.array(z.string()).optional(), + dpop_bound_access_tokens_required: z.boolean().optional() +}); + +/** + * RFC 8414 OAuth 2.0 Authorization Server Metadata + */ +export const OAuthMetadataSchema = z.looseObject({ + issuer: z.string(), + authorization_endpoint: SafeUrlSchema, + token_endpoint: SafeUrlSchema, + registration_endpoint: SafeUrlSchema.optional(), + scopes_supported: z.array(z.string()).optional(), + response_types_supported: z.array(z.string()), + response_modes_supported: z.array(z.string()).optional(), + grant_types_supported: z.array(z.string()).optional(), + token_endpoint_auth_methods_supported: z.array(z.string()).optional(), + token_endpoint_auth_signing_alg_values_supported: z.array(z.string()).optional(), + service_documentation: SafeUrlSchema.optional(), + revocation_endpoint: SafeUrlSchema.optional(), + revocation_endpoint_auth_methods_supported: z.array(z.string()).optional(), + revocation_endpoint_auth_signing_alg_values_supported: z.array(z.string()).optional(), + introspection_endpoint: z.string().optional(), + introspection_endpoint_auth_methods_supported: z.array(z.string()).optional(), + introspection_endpoint_auth_signing_alg_values_supported: z.array(z.string()).optional(), + code_challenge_methods_supported: z.array(z.string()).optional(), + client_id_metadata_document_supported: z.boolean().optional() +}); + +/** + * OpenID Connect Discovery 1.0 Provider Metadata + * + * @see https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata + */ +export const OpenIdProviderMetadataSchema = z.looseObject({ + issuer: z.string(), + authorization_endpoint: SafeUrlSchema, + token_endpoint: SafeUrlSchema, + userinfo_endpoint: SafeUrlSchema.optional(), + jwks_uri: SafeUrlSchema, + registration_endpoint: SafeUrlSchema.optional(), + scopes_supported: z.array(z.string()).optional(), + response_types_supported: z.array(z.string()), + response_modes_supported: z.array(z.string()).optional(), + grant_types_supported: z.array(z.string()).optional(), + acr_values_supported: z.array(z.string()).optional(), + subject_types_supported: z.array(z.string()), + id_token_signing_alg_values_supported: z.array(z.string()), + id_token_encryption_alg_values_supported: z.array(z.string()).optional(), + id_token_encryption_enc_values_supported: z.array(z.string()).optional(), + userinfo_signing_alg_values_supported: z.array(z.string()).optional(), + userinfo_encryption_alg_values_supported: z.array(z.string()).optional(), + userinfo_encryption_enc_values_supported: z.array(z.string()).optional(), + request_object_signing_alg_values_supported: z.array(z.string()).optional(), + request_object_encryption_alg_values_supported: z.array(z.string()).optional(), + request_object_encryption_enc_values_supported: z.array(z.string()).optional(), + token_endpoint_auth_methods_supported: z.array(z.string()).optional(), + token_endpoint_auth_signing_alg_values_supported: z.array(z.string()).optional(), + display_values_supported: z.array(z.string()).optional(), + claim_types_supported: z.array(z.string()).optional(), + claims_supported: z.array(z.string()).optional(), + service_documentation: z.string().optional(), + claims_locales_supported: z.array(z.string()).optional(), + ui_locales_supported: z.array(z.string()).optional(), + claims_parameter_supported: z.boolean().optional(), + request_parameter_supported: z.boolean().optional(), + request_uri_parameter_supported: z.boolean().optional(), + require_request_uri_registration: z.boolean().optional(), + op_policy_uri: SafeUrlSchema.optional(), + op_tos_uri: SafeUrlSchema.optional(), + client_id_metadata_document_supported: z.boolean().optional() +}); + +/** + * OpenID Connect Discovery metadata that may include OAuth 2.0 fields + * This schema represents the real-world scenario where OIDC providers + * return a mix of OpenID Connect and OAuth 2.0 metadata fields + */ +export const OpenIdProviderDiscoveryMetadataSchema = z.object({ + ...OpenIdProviderMetadataSchema.shape, + ...OAuthMetadataSchema.pick({ + code_challenge_methods_supported: true + }).shape +}); + +/** + * OAuth 2.1 token response + */ +export const OAuthTokensSchema = z + .object({ + access_token: z.string(), + id_token: z.string().optional(), // Optional for OAuth 2.1, but necessary in OpenID Connect + token_type: z.string(), + expires_in: z.coerce.number().optional(), + scope: z.string().optional(), + refresh_token: z.string().optional() + }) + .strip(); + +/** + * RFC 8693 §2.2.1 Token Exchange response for ID-JAG tokens. + * + * `token_type` is intentionally optional: per RFC 8693 §2.2.1 it is informational when + * the issued token is not an access token, and per RFC 6749 §5.1 it is case-insensitive, + * so strict checking rejects conformant IdPs. + */ +export const IdJagTokenExchangeResponseSchema = z + .object({ + issued_token_type: z.literal('urn:ietf:params:oauth:token-type:id-jag'), + access_token: z.string(), + token_type: z.string().optional(), + expires_in: z.number().optional(), + scope: z.string().optional() + }) + .strip(); + +export type IdJagTokenExchangeResponse = z.infer; + +/** + * OAuth 2.1 error response + */ +export const OAuthErrorResponseSchema = z.object({ + error: z.string(), + error_description: z.string().optional(), + error_uri: z.string().optional() +}); + +/** + * Optional version of {@linkcode SafeUrlSchema} that allows empty string for backward compatibility on `tos_uri` and `logo_uri` + */ +// eslint-disable-next-line unicorn/no-useless-undefined +export const OptionalSafeUrlSchema = SafeUrlSchema.optional().or(z.literal('').transform(() => undefined)); + +/** + * RFC 7591 OAuth 2.0 Dynamic Client Registration metadata + */ +export const OAuthClientMetadataSchema = z + .object({ + redirect_uris: z.array(SafeUrlSchema), + token_endpoint_auth_method: z.string().optional(), + grant_types: z.array(z.string()).optional(), + response_types: z.array(z.string()).optional(), + client_name: z.string().optional(), + client_uri: SafeUrlSchema.optional(), + logo_uri: OptionalSafeUrlSchema, + scope: z.string().optional(), + contacts: z.array(z.string()).optional(), + tos_uri: OptionalSafeUrlSchema, + policy_uri: z.string().optional(), + jwks_uri: SafeUrlSchema.optional(), + jwks: z.any().optional(), + software_id: z.string().optional(), + software_version: z.string().optional(), + software_statement: z.string().optional() + }) + .strip(); + +/** + * RFC 7591 OAuth 2.0 Dynamic Client Registration client information + */ +export const OAuthClientInformationSchema = z + .object({ + client_id: z.string(), + client_secret: z.string().optional(), + client_id_issued_at: z.number().optional(), + client_secret_expires_at: z.number().optional() + }) + .strip(); + +/** + * RFC 7591 OAuth 2.0 Dynamic Client Registration full response (client information plus metadata) + */ +export const OAuthClientInformationFullSchema = OAuthClientMetadataSchema.merge(OAuthClientInformationSchema); + +/** + * RFC 7591 OAuth 2.0 Dynamic Client Registration error response + */ +export const OAuthClientRegistrationErrorSchema = z + .object({ + error: z.string(), + error_description: z.string().optional() + }) + .strip(); + +/** + * RFC 7009 OAuth 2.0 Token Revocation request + */ +export const OAuthTokenRevocationRequestSchema = z + .object({ + token: z.string(), + token_type_hint: z.string().optional() + }) + .strip(); + +export type OAuthMetadata = z.infer; +export type OpenIdProviderMetadata = z.infer; +export type OpenIdProviderDiscoveryMetadata = z.infer; + +export type OAuthTokens = z.infer; +export type OAuthErrorResponse = z.infer; +export type OAuthClientMetadata = z.infer; +export type OAuthClientInformation = z.infer; +export type OAuthClientInformationFull = z.infer; +export type OAuthClientInformationMixed = OAuthClientInformation | OAuthClientInformationFull; +export type OAuthClientRegistrationError = z.infer; +export type OAuthTokenRevocationRequest = z.infer; +export type OAuthProtectedResourceMetadata = z.infer; + +// Unified type for authorization server metadata +export type AuthorizationServerMetadata = OAuthMetadata | OpenIdProviderDiscoveryMetadata; diff --git a/packages/core-internal/src/shared/authUtils.ts b/packages/core-internal/src/shared/authUtils.ts new file mode 100644 index 0000000000..3083e425b9 --- /dev/null +++ b/packages/core-internal/src/shared/authUtils.ts @@ -0,0 +1,57 @@ +/** + * Utilities for handling OAuth resource URIs. + */ + +/** + * Converts a server URL to a resource URL by removing the fragment. + * {@link https://datatracker.ietf.org/doc/html/rfc8707#section-2 | RFC 8707 section 2} + * states that resource URIs "MUST NOT include a fragment component". + * Keeps everything else unchanged (scheme, domain, port, path, query). + */ +export function resourceUrlFromServerUrl(url: URL | string): URL { + const resourceURL = typeof url === 'string' ? new URL(url) : new URL(url.href); + resourceURL.hash = ''; // Remove fragment + return resourceURL; +} + +/** + * Checks if a requested resource URL matches a configured resource URL. + * A requested resource matches if it has the same scheme, domain, port, + * and its path starts with the configured resource's path. + * + * @param options - The options object + * @param options.requestedResource - The resource URL being requested + * @param options.configuredResource - The resource URL that has been configured + * @returns true if the requested resource matches the configured resource, false otherwise + */ +export function checkResourceAllowed({ + requestedResource, + configuredResource +}: { + requestedResource: URL | string; + configuredResource: URL | string; +}): boolean { + const requested = typeof requestedResource === 'string' ? new URL(requestedResource) : new URL(requestedResource.href); + const configured = typeof configuredResource === 'string' ? new URL(configuredResource) : new URL(configuredResource.href); + + // Compare the origin (scheme, domain, and port) + if (requested.origin !== configured.origin) { + return false; + } + + // Handle cases like requested=/foo and configured=/foo/ + if (requested.pathname.length < configured.pathname.length) { + return false; + } + + // Check if the requested path starts with the configured path + // Ensure both paths end with / for proper comparison + // This ensures that if we have paths like "/api" and "/api/users", + // we properly detect that "/api/users" is a subpath of "/api" + // By adding a trailing slash if missing, we avoid false positives + // where paths like "/api123" would incorrectly match "/api" + const requestedPath = requested.pathname.endsWith('/') ? requested.pathname : requested.pathname + '/'; + const configuredPath = configured.pathname.endsWith('/') ? configured.pathname : configured.pathname + '/'; + + return requestedPath.startsWith(configuredPath); +} diff --git a/packages/core-internal/src/shared/metadataUtils.ts b/packages/core-internal/src/shared/metadataUtils.ts new file mode 100644 index 0000000000..0836b4394a --- /dev/null +++ b/packages/core-internal/src/shared/metadataUtils.ts @@ -0,0 +1,26 @@ +import type { BaseMetadata } from '../types/index'; + +/** + * Utilities for working with {@linkcode BaseMetadata} objects. + */ + +/** + * Gets the display name for an object with {@linkcode BaseMetadata}. + * For tools, the precedence is: `title` → {@linkcode index.ToolAnnotations | annotations}.`title` → `name` + * For other objects: `title` → `name` + * This implements the spec requirement: "if no title is provided, name should be used for display purposes" + */ +export function getDisplayName(metadata: BaseMetadata | (BaseMetadata & { annotations?: { title?: string } })): string { + // First check for title (not undefined and not empty string) + if (metadata.title !== undefined && metadata.title !== '') { + return metadata.title; + } + + // Then check for annotations.title (only present in Tool objects) + if ('annotations' in metadata && metadata.annotations?.title) { + return metadata.annotations.title; + } + + // Finally fall back to name + return metadata.name; +} diff --git a/packages/core-internal/src/shared/protocol.examples.ts b/packages/core-internal/src/shared/protocol.examples.ts new file mode 100644 index 0000000000..0ae10e6d08 --- /dev/null +++ b/packages/core-internal/src/shared/protocol.examples.ts @@ -0,0 +1,29 @@ +/** + * Type-checked examples for `protocol.ts`. + * + * These examples are synced into JSDoc comments via the sync-snippets script. + * Each function's region markers define the code snippet that appears in the docs. + * + * @module + */ + +import * as z from 'zod/v4'; + +import type { BaseContext, Protocol } from './protocol'; + +/** + * Example: registering a handler for a custom (non-spec) request method. + */ +function Protocol_setRequestHandler_customMethod(protocol: Protocol) { + //#region Protocol_setRequestHandler_customMethod + const SearchParams = z.object({ query: z.string(), limit: z.number().optional() }); + const SearchResult = z.object({ hits: z.array(z.string()) }); + + protocol.setRequestHandler('acme/search', { params: SearchParams, result: SearchResult }, async (params, _ctx) => { + return { hits: [`result for ${params.query}`] }; + }); + //#endregion Protocol_setRequestHandler_customMethod + void protocol; +} + +void Protocol_setRequestHandler_customMethod; diff --git a/packages/core-internal/src/shared/protocol.ts b/packages/core-internal/src/shared/protocol.ts new file mode 100644 index 0000000000..3b7efec2a4 --- /dev/null +++ b/packages/core-internal/src/shared/protocol.ts @@ -0,0 +1,1066 @@ +import { SdkError, SdkErrorCode } from '../errors/sdkErrors'; +import type { + AuthInfo, + CancelledNotification, + ClientCapabilities, + CreateMessageRequest, + CreateMessageResult, + CreateMessageResultWithTools, + ElicitRequestFormParams, + ElicitRequestURLParams, + ElicitResult, + JSONRPCErrorResponse, + JSONRPCNotification, + JSONRPCRequest, + JSONRPCResponse, + JSONRPCResultResponse, + LoggingLevel, + MessageExtraInfo, + Notification, + NotificationMethod, + NotificationTypeMap, + Progress, + ProgressNotification, + Request, + RequestId, + RequestMeta, + RequestMethod, + RequestTypeMap, + Result, + ResultTypeMap, + ServerCapabilities +} from '../types/index'; +import { + getNotificationSchema, + getRequestSchema, + getResultSchema, + isJSONRPCErrorResponse, + isJSONRPCNotification, + isJSONRPCRequest, + isJSONRPCResultResponse, + ProtocolError, + ProtocolErrorCode, + SUPPORTED_PROTOCOL_VERSIONS +} from '../types/index'; +import type { StandardSchemaV1 } from '../util/standardSchema'; +import { isStandardSchema, validateStandardSchema } from '../util/standardSchema'; +import type { Transport, TransportSendOptions } from './transport'; + +/** + * Callback for progress notifications. + */ +export type ProgressCallback = (progress: Progress) => void; + +/** + * Additional initialization options. + */ +export type ProtocolOptions = { + /** + * Protocol versions supported. First version is preferred (sent by client, + * used as fallback by server). Passed to transport during {@linkcode Protocol.connect | connect()}. + * + * @default {@linkcode SUPPORTED_PROTOCOL_VERSIONS} + */ + supportedProtocolVersions?: string[]; + + /** + * Whether to restrict emitted requests to only those that the remote side has indicated that they can handle, through their advertised capabilities. + * + * Note that this DOES NOT affect checking of _local_ side capabilities, as it is considered a logic error to mis-specify those. + * + * Currently this defaults to `false`, for backwards compatibility with SDK versions that did not advertise capabilities correctly. In future, this will default to `true`. + */ + enforceStrictCapabilities?: boolean; + /** + * An array of notification method names that should be automatically debounced. + * Any notifications with a method in this list will be coalesced if they + * occur in the same tick of the event loop. + * e.g., `['notifications/tools/list_changed']` + */ + debouncedNotificationMethods?: string[]; +}; + +/** + * The default request timeout, in milliseconds. + */ +export const DEFAULT_REQUEST_TIMEOUT_MSEC = 60_000; + +/** + * Options that can be given per request. + */ +export type RequestOptions = { + /** + * If set, requests progress notifications from the remote end (if supported). When progress notifications are received, this callback will be invoked. + */ + onprogress?: ProgressCallback; + + /** + * Can be used to cancel an in-flight request. This will cause an `AbortError` to be raised from {@linkcode Protocol.request | request()}. + */ + signal?: AbortSignal; + + /** + * A timeout (in milliseconds) for this request. If exceeded, an {@linkcode SdkError} with code {@linkcode SdkErrorCode.RequestTimeout} will be raised from {@linkcode Protocol.request | request()}. + * + * If not specified, {@linkcode DEFAULT_REQUEST_TIMEOUT_MSEC} will be used as the timeout. + */ + timeout?: number; + + /** + * If `true`, receiving a progress notification will reset the request timeout. + * This is useful for long-running operations that send periodic progress updates. + * Default: `false` + */ + resetTimeoutOnProgress?: boolean; + + /** + * Maximum total time (in milliseconds) to wait for a response. + * If exceeded, an {@linkcode SdkError} with code {@linkcode SdkErrorCode.RequestTimeout} will be raised, regardless of progress notifications. + * If not specified, there is no maximum total timeout. + */ + maxTotalTimeout?: number; +} & TransportSendOptions; + +/** + * Options that can be given per notification. + */ +export type NotificationOptions = { + /** + * May be used to indicate to the transport which incoming request to associate this outgoing notification with. + */ + relatedRequestId?: RequestId; +}; + +/** + * Base context provided to all request handlers. + */ +export type BaseContext = { + /** + * The session ID from the transport, if available. + */ + sessionId?: string; + + /** + * Information about the MCP request being handled. + */ + mcpReq: { + /** + * The JSON-RPC ID of the request being handled. + */ + id: RequestId; + + /** + * The method name of the request (e.g., 'tools/call', 'ping'). + */ + method: string; + + /** + * Metadata from the original request. + */ + _meta?: RequestMeta; + + /** + * An abort signal used to communicate if the request was cancelled from the sender's side. + */ + signal: AbortSignal; + + /** + * Sends a request that relates to the current request being handled. + * + * This is used by certain transports to correctly associate related messages. + * + * For spec methods the result type is inferred from the method name. + * For custom (non-spec) methods, pass a result schema as the second argument. + */ + send: { + ( + request: { method: M; params?: Record }, + options?: RequestOptions + ): Promise; + ( + request: Request, + resultSchema: T, + options?: RequestOptions + ): Promise>; + }; + + /** + * Sends a notification that relates to the current request being handled. + * + * This is used by certain transports to correctly associate related messages. + */ + notify: (notification: Notification) => Promise; + }; + + /** + * HTTP transport information, only available when using an HTTP-based transport. + */ + http?: { + /** + * Information about a validated access token, provided to request handlers. + */ + authInfo?: AuthInfo; + }; +}; + +/** + * Context provided to server-side request handlers, extending {@linkcode BaseContext} with server-specific fields. + */ +export type ServerContext = BaseContext & { + mcpReq: { + /** + * Send a log message notification to the client. + * Respects the client's log level filter set via logging/setLevel. + * + * @deprecated Deprecated as of protocol version 2026-07-28 (SEP-2577). + * Remains functional during the deprecation window (at least twelve months). + * Migrate to stderr logging (STDIO servers) or OpenTelemetry. + */ + log: (level: LoggingLevel, data: unknown, logger?: string) => Promise; + + /** + * Send an elicitation request to the client, requesting user input. + */ + elicitInput: (params: ElicitRequestFormParams | ElicitRequestURLParams, options?: RequestOptions) => Promise; + + /** + * Request LLM sampling from the client. + * + * @deprecated Deprecated as of protocol version 2026-07-28 (SEP-2577). + * Remains functional during the deprecation window (at least twelve months). + * Migrate to calling LLM provider APIs directly. + */ + requestSampling: ( + params: CreateMessageRequest['params'], + options?: RequestOptions + ) => Promise; + }; + + http?: { + /** + * The original HTTP request. + */ + req?: globalThis.Request; + + /** + * Closes the SSE stream for this request, triggering client reconnection. + * Only available when using a StreamableHTTPServerTransport with eventStore configured. + */ + closeSSE?: () => void; + + /** + * Closes the standalone GET SSE stream, triggering client reconnection. + * Only available when using a StreamableHTTPServerTransport with eventStore configured. + */ + closeStandaloneSSE?: () => void; + }; +}; + +/** + * Context provided to client-side request handlers. + */ +export type ClientContext = BaseContext; + +/** + * Information about a request's timeout state + */ +type TimeoutInfo = { + timeoutId: ReturnType; + startTime: number; + timeout: number; + maxTotalTimeout?: number; + resetTimeoutOnProgress: boolean; + onTimeout: () => void; +}; + +/** + * Implements MCP protocol framing on top of a pluggable transport, including + * features like request/response linking, notifications, and progress. + * + * `Protocol` is abstract; `Client` and `Server` are the concrete role-specific + * implementations most code should use. + */ +export abstract class Protocol { + private _transport?: Transport; + private _requestMessageId = 0; + private _requestHandlers: Map Promise> = new Map(); + private _requestHandlerAbortControllers: Map = new Map(); + private _notificationHandlers: Map Promise> = new Map(); + private _responseHandlers: Map void> = new Map(); + private _progressHandlers: Map = new Map(); + private _timeoutInfo: Map = new Map(); + private _pendingDebouncedNotifications = new Set(); + + protected _supportedProtocolVersions: string[]; + + /** + * Callback for when the connection is closed for any reason. + * + * This is invoked when {@linkcode Protocol.close | close()} is called as well. + */ + onclose?: () => void; + + /** + * Callback for when an error occurs. + * + * Note that errors are not necessarily fatal; they are used for reporting any kind of exceptional condition out of band. + */ + onerror?: (error: Error) => void; + + /** + * A handler to invoke for any request types that do not have their own handler installed. + */ + fallbackRequestHandler?: (request: JSONRPCRequest, ctx: ContextT) => Promise; + + /** + * A handler to invoke for any notification types that do not have their own handler installed. + */ + fallbackNotificationHandler?: (notification: Notification) => Promise; + + constructor(private _options?: ProtocolOptions) { + this._supportedProtocolVersions = _options?.supportedProtocolVersions ?? SUPPORTED_PROTOCOL_VERSIONS; + + this.setNotificationHandler('notifications/cancelled', notification => { + this._oncancel(notification); + }); + + this.setNotificationHandler('notifications/progress', notification => { + this._onprogress(notification); + }); + + this.setRequestHandler( + 'ping', + // Automatic pong by default. + _request => ({}) as Result + ); + } + + /** + * Builds the context object for request handlers. Subclasses must override + * to return the appropriate context type (e.g., ServerContext adds HTTP request info). + */ + protected abstract buildContext(ctx: BaseContext, transportInfo?: MessageExtraInfo): ContextT; + + private async _oncancel(notification: CancelledNotification): Promise { + if (!notification.params.requestId) { + return; + } + // Handle request cancellation + const controller = this._requestHandlerAbortControllers.get(notification.params.requestId); + controller?.abort(notification.params.reason); + } + + private _setupTimeout( + messageId: number, + timeout: number, + maxTotalTimeout: number | undefined, + onTimeout: () => void, + resetTimeoutOnProgress: boolean = false + ) { + this._timeoutInfo.set(messageId, { + timeoutId: setTimeout(onTimeout, timeout), + startTime: Date.now(), + timeout, + maxTotalTimeout, + resetTimeoutOnProgress, + onTimeout + }); + } + + private _resetTimeout(messageId: number): boolean { + const info = this._timeoutInfo.get(messageId); + if (!info) return false; + + const totalElapsed = Date.now() - info.startTime; + if (info.maxTotalTimeout && totalElapsed >= info.maxTotalTimeout) { + this._timeoutInfo.delete(messageId); + throw new SdkError(SdkErrorCode.RequestTimeout, 'Maximum total timeout exceeded', { + maxTotalTimeout: info.maxTotalTimeout, + totalElapsed + }); + } + + clearTimeout(info.timeoutId); + info.timeoutId = setTimeout(info.onTimeout, info.timeout); + return true; + } + + private _cleanupTimeout(messageId: number) { + const info = this._timeoutInfo.get(messageId); + if (info) { + clearTimeout(info.timeoutId); + this._timeoutInfo.delete(messageId); + } + } + + /** + * Attaches to the given transport, starts it, and starts listening for messages. + * + * The caller assumes ownership of the {@linkcode Transport}, replacing any callbacks that have already been set, and expects that it is the only user of the {@linkcode Transport} instance going forward. + */ + async connect(transport: Transport): Promise { + this._transport = transport; + const _onclose = this.transport?.onclose; + this._transport.onclose = () => { + try { + _onclose?.(); + } finally { + this._onclose(); + } + }; + + const _onerror = this.transport?.onerror; + this._transport.onerror = (error: Error) => { + _onerror?.(error); + this._onerror(error); + }; + + const _onmessage = this._transport?.onmessage; + this._transport.onmessage = (message, extra) => { + _onmessage?.(message, extra); + if (isJSONRPCResultResponse(message) || isJSONRPCErrorResponse(message)) { + this._onresponse(message); + } else if (isJSONRPCRequest(message)) { + this._onrequest(message, extra); + } else if (isJSONRPCNotification(message)) { + this._onnotification(message); + } else { + this._onerror(new Error(`Unknown message type: ${JSON.stringify(message)}`)); + } + }; + + // Pass supported protocol versions to transport for header validation + transport.setSupportedProtocolVersions?.(this._supportedProtocolVersions); + + await this._transport.start(); + } + + private _onclose(): void { + const responseHandlers = this._responseHandlers; + this._responseHandlers = new Map(); + this._progressHandlers.clear(); + this._pendingDebouncedNotifications.clear(); + + for (const info of this._timeoutInfo.values()) { + clearTimeout(info.timeoutId); + } + this._timeoutInfo.clear(); + + const requestHandlerAbortControllers = this._requestHandlerAbortControllers; + this._requestHandlerAbortControllers = new Map(); + + const error = new SdkError(SdkErrorCode.ConnectionClosed, 'Connection closed'); + + this._transport = undefined; + + try { + this.onclose?.(); + } finally { + for (const handler of responseHandlers.values()) { + handler(error); + } + + for (const controller of requestHandlerAbortControllers.values()) { + controller.abort(error); + } + } + } + + private _onerror(error: Error): void { + this.onerror?.(error); + } + + private _onnotification(notification: JSONRPCNotification): void { + const handler = this._notificationHandlers.get(notification.method) ?? this.fallbackNotificationHandler; + + // Ignore notifications not being subscribed to. + if (handler === undefined) { + return; + } + + // Starting with Promise.resolve() puts any synchronous errors into the monad as well. + Promise.resolve() + .then(() => handler(notification)) + .catch(error => this._onerror(new Error(`Uncaught error in notification handler: ${error}`))); + } + + private _onrequest(request: JSONRPCRequest, extra?: MessageExtraInfo): void { + const handler = this._requestHandlers.get(request.method) ?? this.fallbackRequestHandler; + + // Capture the current transport at request time to ensure responses go to the correct client + const capturedTransport = this._transport; + + const sendNotification = (notification: Notification, options?: NotificationOptions) => + this.notification(notification, { ...options, relatedRequestId: request.id }); + const sendRequest = (r: Request, resultSchema: U, options?: RequestOptions) => + this._requestWithSchema(r, resultSchema, { ...options, relatedRequestId: request.id }); + + if (handler === undefined) { + const errorResponse: JSONRPCErrorResponse = { + jsonrpc: '2.0', + id: request.id, + error: { + code: ProtocolErrorCode.MethodNotFound, + message: 'Method not found' + } + }; + capturedTransport?.send(errorResponse).catch(error => this._onerror(new Error(`Failed to send an error response: ${error}`))); + return; + } + + const abortController = new AbortController(); + this._requestHandlerAbortControllers.set(request.id, abortController); + + const baseCtx: BaseContext = { + sessionId: capturedTransport?.sessionId, + mcpReq: { + id: request.id, + method: request.method, + _meta: request.params?._meta, + signal: abortController.signal, + // BaseContext.mcpReq.send is declared with two overloads (spec-method-keyed and explicit-schema). Arrow + // literals can't carry overload signatures, so the inferred single-signature type isn't assignable to + // that overloaded property type. The cast is sound: this impl dispatches both overload paths via the + // isStandardSchema guard, and sendRequest validates the result against the resolved schema either way. + send: ((r: Request, schemaOrOptions?: StandardSchemaV1 | RequestOptions, maybeOptions?: RequestOptions) => { + if (isStandardSchema(schemaOrOptions)) { + return sendRequest(r, schemaOrOptions, maybeOptions); + } + const resultSchema = getResultSchema(r.method); + if (!resultSchema) { + throw new TypeError( + `'${r.method}' is not a spec method; pass a result schema as the second argument to ctx.mcpReq.send().` + ); + } + return sendRequest(r, resultSchema, schemaOrOptions); + }) as BaseContext['mcpReq']['send'], + notify: sendNotification + }, + http: extra?.authInfo ? { authInfo: extra.authInfo } : undefined + }; + const ctx = this.buildContext(baseCtx, extra); + + // Starting with Promise.resolve() puts any synchronous errors into the monad as well. + Promise.resolve() + .then(() => handler(request, ctx)) + .then( + async result => { + if (abortController.signal.aborted) { + // Request was cancelled + return; + } + + const response: JSONRPCResponse = { + result, + jsonrpc: '2.0', + id: request.id + }; + await capturedTransport?.send(response); + }, + async error => { + if (abortController.signal.aborted) { + // Request was cancelled + return; + } + + const errorResponse: JSONRPCErrorResponse = { + jsonrpc: '2.0', + id: request.id, + error: { + code: Number.isSafeInteger(error['code']) ? error['code'] : ProtocolErrorCode.InternalError, + message: error.message ?? 'Internal error', + ...(error['data'] !== undefined && { data: error['data'] }) + } + }; + await capturedTransport?.send(errorResponse); + } + ) + .catch(error => this._onerror(new Error(`Failed to send response: ${error}`))) + .finally(() => { + if (this._requestHandlerAbortControllers.get(request.id) === abortController) { + this._requestHandlerAbortControllers.delete(request.id); + } + }); + } + + private _onprogress(notification: ProgressNotification): void { + const { progressToken, ...params } = notification.params; + const messageId = Number(progressToken); + + const handler = this._progressHandlers.get(messageId); + if (!handler) { + this._onerror(new Error(`Received a progress notification for an unknown token: ${JSON.stringify(notification)}`)); + return; + } + + const responseHandler = this._responseHandlers.get(messageId); + const timeoutInfo = this._timeoutInfo.get(messageId); + + if (timeoutInfo && responseHandler && timeoutInfo.resetTimeoutOnProgress) { + try { + this._resetTimeout(messageId); + } catch (error) { + // Clean up if maxTotalTimeout was exceeded + this._responseHandlers.delete(messageId); + this._progressHandlers.delete(messageId); + this._cleanupTimeout(messageId); + responseHandler(error as Error); + return; + } + } + + handler(params); + } + + private _onresponse(response: JSONRPCResponse | JSONRPCErrorResponse): void { + const messageId = Number(response.id); + + const handler = this._responseHandlers.get(messageId); + if (handler === undefined) { + this._onerror(new Error(`Received a response for an unknown message ID: ${JSON.stringify(response)}`)); + return; + } + + this._responseHandlers.delete(messageId); + this._cleanupTimeout(messageId); + this._progressHandlers.delete(messageId); + + if (isJSONRPCResultResponse(response)) { + handler(response); + } else { + const error = ProtocolError.fromError(response.error.code, response.error.message, response.error.data); + handler(error); + } + } + + get transport(): Transport | undefined { + return this._transport; + } + + /** + * Closes the connection. + */ + async close(): Promise { + await this._transport?.close(); + } + + /** + * A method to check if a capability is supported by the remote side, for the given method to be called. + * + * This should be implemented by subclasses. + */ + protected abstract assertCapabilityForMethod(method: RequestMethod | string): void; + + /** + * A method to check if a notification is supported by the local side, for the given method to be sent. + * + * This should be implemented by subclasses. + */ + protected abstract assertNotificationCapability(method: NotificationMethod | string): void; + + /** + * A method to check if a request handler is supported by the local side, for the given method to be handled. + * + * This should be implemented by subclasses. + */ + protected abstract assertRequestHandlerCapability(method: string): void; + + /** + * Sends a request and waits for a response. + * + * For spec methods the result schema is resolved automatically from the method name + * and the return type is method-keyed. For custom (non-spec) methods, pass a + * `resultSchema` as the second argument; the response is validated against it and + * the return type is inferred from the schema. + * + * Do not use this method to emit notifications! Use {@linkcode Protocol.notification | notification()} instead. + */ + request( + request: { method: M; params?: Record }, + options?: RequestOptions + ): Promise; + request( + request: Request, + resultSchema: T, + options?: RequestOptions + ): Promise>; + request(request: Request, schemaOrOptions?: StandardSchemaV1 | RequestOptions, maybeOptions?: RequestOptions): Promise { + if (isStandardSchema(schemaOrOptions)) { + return this._requestWithSchema(request, schemaOrOptions, maybeOptions); + } + const resultSchema = getResultSchema(request.method); + if (!resultSchema) { + throw new TypeError(`'${request.method}' is not a spec method; pass a result schema as the second argument to request().`); + } + return this._requestWithSchema(request, resultSchema, schemaOrOptions); + } + + /** + * Sends a request and waits for a response, using the provided schema for validation. + * + * This is the internal implementation used by SDK methods that need to specify + * a particular result schema (e.g., for compatibility schemas). + */ + protected _requestWithSchema( + request: Request, + resultSchema: T, + options?: RequestOptions + ): Promise> { + const { relatedRequestId, resumptionToken, onresumptiontoken } = options ?? {}; + + let onAbort: (() => void) | undefined; + let cleanupMessageId: number | undefined; + + // Send the request + return new Promise>((resolve, reject) => { + const earlyReject = (error: unknown) => { + reject(error); + }; + + if (!this._transport) { + earlyReject(new Error('Not connected')); + return; + } + + if (this._options?.enforceStrictCapabilities === true) { + try { + this.assertCapabilityForMethod(request.method); + } catch (error) { + earlyReject(error); + return; + } + } + + options?.signal?.throwIfAborted(); + + const messageId = this._requestMessageId++; + cleanupMessageId = messageId; + const jsonrpcRequest: JSONRPCRequest = { + ...request, + jsonrpc: '2.0', + id: messageId + }; + + if (options?.onprogress) { + this._progressHandlers.set(messageId, options.onprogress); + jsonrpcRequest.params = { + ...request.params, + _meta: { + ...request.params?._meta, + progressToken: messageId + } + }; + } + + let responseReceived = false; + + const cancel = (reason: unknown) => { + if (responseReceived) { + return; + } + this._progressHandlers.delete(messageId); + + this._transport + ?.send( + { + jsonrpc: '2.0', + method: 'notifications/cancelled', + params: { + requestId: messageId, + reason: String(reason) + } + }, + { relatedRequestId, resumptionToken, onresumptiontoken } + ) + .catch(error => this._onerror(new Error(`Failed to send cancellation: ${error}`))); + + // Wrap the reason in an SdkError if it isn't already + const error = reason instanceof SdkError ? reason : new SdkError(SdkErrorCode.RequestTimeout, String(reason)); + reject(error); + }; + + this._responseHandlers.set(messageId, response => { + if (options?.signal?.aborted) { + return; + } + responseReceived = true; + + if (response instanceof Error) { + return reject(response); + } + + validateStandardSchema(resultSchema, response.result).then(parseResult => { + if (parseResult.success) { + resolve(parseResult.data); + } else { + reject(new SdkError(SdkErrorCode.InvalidResult, `Invalid result for ${request.method}: ${parseResult.error}`)); + } + }, reject); + }); + + onAbort = () => cancel(options?.signal?.reason); + options?.signal?.addEventListener('abort', onAbort, { once: true }); + + const timeout = options?.timeout ?? DEFAULT_REQUEST_TIMEOUT_MSEC; + const timeoutHandler = () => cancel(new SdkError(SdkErrorCode.RequestTimeout, 'Request timed out', { timeout })); + + this._setupTimeout(messageId, timeout, options?.maxTotalTimeout, timeoutHandler, options?.resetTimeoutOnProgress ?? false); + + this._transport.send(jsonrpcRequest, { relatedRequestId, resumptionToken, onresumptiontoken }).catch(error => { + this._progressHandlers.delete(messageId); + reject(error); + }); + }).finally(() => { + // Per-request cleanup that must run on every exit path. Consolidated + // here so new exit paths added to the promise body can't forget it. + // _progressHandlers is NOT cleaned up here: _onresponse deletes it + // on resolution, and error paths above delete it inline. + if (onAbort) { + options?.signal?.removeEventListener('abort', onAbort); + } + if (cleanupMessageId !== undefined) { + this._responseHandlers.delete(cleanupMessageId); + this._cleanupTimeout(cleanupMessageId); + } + }); + } + + /** + * Emits a notification, which is a one-way message that does not expect a response. + */ + async notification(notification: Notification, options?: NotificationOptions): Promise { + if (!this._transport) { + throw new SdkError(SdkErrorCode.NotConnected, 'Not connected'); + } + + this.assertNotificationCapability(notification.method); + + const jsonrpcNotification: JSONRPCNotification = { jsonrpc: '2.0', ...notification }; + + const debouncedMethods = this._options?.debouncedNotificationMethods ?? []; + // A notification can only be debounced if it's in the list AND it's "simple" + // (i.e., has no parameters and no related request ID that could be lost). + const canDebounce = debouncedMethods.includes(notification.method) && !notification.params && !options?.relatedRequestId; + + if (canDebounce) { + // If a notification of this type is already scheduled, do nothing. + if (this._pendingDebouncedNotifications.has(notification.method)) { + return; + } + + // Mark this notification type as pending. + this._pendingDebouncedNotifications.add(notification.method); + + // Schedule the actual send to happen in the next microtask. + // This allows all synchronous calls in the current event loop tick to be coalesced. + Promise.resolve().then(() => { + // Un-mark the notification so the next one can be scheduled. + this._pendingDebouncedNotifications.delete(notification.method); + + // SAFETY CHECK: If the connection was closed while this was pending, abort. + if (!this._transport) { + return; + } + + // Send the notification, but don't await it here to avoid blocking. + // Handle potential errors with a .catch(). + this._transport?.send(jsonrpcNotification, options).catch(error => this._onerror(error)); + }); + + // Return immediately. + return; + } + + await this._transport.send(jsonrpcNotification, options); + } + + /** + * Registers a handler to invoke when this protocol object receives a request with the given method. + * + * Note that this will replace any previous request handler for the same method. + * + * For spec methods, pass `(method, handler)`; the request is parsed with the spec + * schema and the handler receives the typed `Request`. For custom (non-spec) + * methods, pass `(method, schemas, handler)`; `params` are validated against + * `schemas.params` and the handler receives the parsed params object directly. + * Supplying `schemas.result` types the handler's return value. + * + * @example Custom request method + * ```ts source="./protocol.examples.ts#Protocol_setRequestHandler_customMethod" + * const SearchParams = z.object({ query: z.string(), limit: z.number().optional() }); + * const SearchResult = z.object({ hits: z.array(z.string()) }); + * + * protocol.setRequestHandler('acme/search', { params: SearchParams, result: SearchResult }, async (params, _ctx) => { + * return { hits: [`result for ${params.query}`] }; + * }); + * ``` + */ + setRequestHandler( + method: M, + handler: (request: RequestTypeMap[M], ctx: ContextT) => ResultTypeMap[M] | Promise + ): void; + setRequestHandler

( + method: string, + schemas: { params: P; result?: R }, + handler: (params: StandardSchemaV1.InferOutput

, ctx: ContextT) => InferHandlerResult | Promise> + ): void; + setRequestHandler( + method: string, + schemasOrHandler: RequestHandlerSchemas | ((request: unknown, ctx: ContextT) => Result | Promise), + maybeHandler?: (params: unknown, ctx: ContextT) => Result | Promise + ): void { + this.assertRequestHandlerCapability(method); + + let stored: (request: JSONRPCRequest, ctx: ContextT) => Promise; + + if (typeof schemasOrHandler === 'function') { + const schema = getRequestSchema(method); + if (!schema) { + throw new TypeError( + `'${method}' is not a spec request method; pass schemas as the second argument to setRequestHandler().` + ); + } + stored = (request, ctx) => Promise.resolve(schemasOrHandler(schema.parse(request), ctx)); + } else if (maybeHandler) { + stored = async (request, ctx) => { + const userParams = { ...request.params }; + delete userParams._meta; + const parsed = await validateStandardSchema(schemasOrHandler.params, userParams); + if (!parsed.success) { + throw new ProtocolError(ProtocolErrorCode.InvalidParams, `Invalid params for ${method}: ${parsed.error}`); + } + return maybeHandler(parsed.data, ctx); + }; + } else { + throw new TypeError('setRequestHandler: handler is required'); + } + + this._requestHandlers.set(method, this._wrapHandler(method, stored)); + } + + /** + * Hook for subclasses to wrap a registered request handler with role-specific + * validation or behavior (e.g. `Server` validates `tools/call` results, `Client` + * validates `elicitation/create` mode and result). Runs for both the 2-arg and + * 3-arg registration paths. The default implementation is identity. + * + * Subclasses overriding this hook avoid redeclaring `setRequestHandler`'s overload set. + */ + protected _wrapHandler( + _method: string, + handler: (request: JSONRPCRequest, ctx: ContextT) => Promise + ): (request: JSONRPCRequest, ctx: ContextT) => Promise { + return handler; + } + + /** + * Removes the request handler for the given method. + */ + removeRequestHandler(method: RequestMethod | string): void { + this._requestHandlers.delete(method); + } + + /** + * Asserts that a request handler has not already been set for the given method, in preparation for a new one being automatically installed. + */ + assertCanSetRequestHandler(method: RequestMethod | string): void { + if (this._requestHandlers.has(method)) { + throw new Error(`A request handler for ${method} already exists, which would be overridden`); + } + } + + /** + * Registers a handler to invoke when this protocol object receives a notification with the given method. + * + * Note that this will replace any previous notification handler for the same method. + * + * For spec methods, pass `(method, handler)`; the notification is parsed with the + * spec schema. For custom (non-spec) methods, pass `(method, schemas, handler)`; + * `params` are validated against `schemas.params` and the handler receives the + * parsed params object directly. The raw notification is passed as the second + * argument; `_meta` is recoverable via `notification.params?._meta`. + */ + setNotificationHandler( + method: M, + handler: (notification: NotificationTypeMap[M]) => void | Promise + ): void; + setNotificationHandler

( + method: string, + schemas: { params: P }, + handler: (params: StandardSchemaV1.InferOutput

, notification: Notification) => void | Promise + ): void; + setNotificationHandler( + method: string, + schemasOrHandler: { params: StandardSchemaV1 } | ((notification: unknown) => void | Promise), + maybeHandler?: (params: unknown, notification: Notification) => void | Promise + ): void { + if (typeof schemasOrHandler === 'function') { + const schema = getNotificationSchema(method); + if (!schema) { + throw new TypeError( + `'${method}' is not a spec notification method; pass schemas as the second argument to setNotificationHandler().` + ); + } + this._notificationHandlers.set(method, notification => Promise.resolve(schemasOrHandler(schema.parse(notification)))); + return; + } + + if (!maybeHandler) { + throw new TypeError('setNotificationHandler: handler is required'); + } + this._notificationHandlers.set(method, async notification => { + const userParams = { ...notification.params }; + delete userParams._meta; + const parsed = await validateStandardSchema(schemasOrHandler.params, userParams); + if (!parsed.success) { + throw new ProtocolError(ProtocolErrorCode.InvalidParams, `Invalid params for notification ${method}: ${parsed.error}`); + } + await maybeHandler(parsed.data, notification); + }); + } + + /** + * Removes the notification handler for the given method. + */ + removeNotificationHandler(method: NotificationMethod | string): void { + this._notificationHandlers.delete(method); + } +} + +/** + * Schema bundle accepted by {@linkcode Protocol.setRequestHandler | setRequestHandler}'s 3-arg form. + * + * `params` is required and validates the inbound `request.params`. `result` is optional; + * when supplied it types the handler's return value (no runtime validation is performed + * on the result). + */ +export interface RequestHandlerSchemas< + P extends StandardSchemaV1 = StandardSchemaV1, + R extends StandardSchemaV1 | undefined = StandardSchemaV1 | undefined +> { + params: P; + result?: R; +} + +type InferHandlerResult = R extends StandardSchemaV1 ? StandardSchemaV1.InferOutput : Result; + +function isPlainObject(value: unknown): value is Record { + return value !== null && typeof value === 'object' && !Array.isArray(value); +} + +export function mergeCapabilities(base: ServerCapabilities, additional: Partial): ServerCapabilities; +export function mergeCapabilities(base: ClientCapabilities, additional: Partial): ClientCapabilities; +export function mergeCapabilities(base: T, additional: Partial): T { + const result: T = { ...base }; + for (const key in additional) { + const k = key as keyof T; + const addValue = additional[k]; + if (addValue === undefined) continue; + const baseValue = result[k]; + result[k] = + isPlainObject(baseValue) && isPlainObject(addValue) + ? ({ ...(baseValue as Record), ...(addValue as Record) } as T[typeof k]) + : (addValue as T[typeof k]); + } + return result; +} diff --git a/packages/core-internal/src/shared/stdio.ts b/packages/core-internal/src/shared/stdio.ts new file mode 100644 index 0000000000..8bd794b87b --- /dev/null +++ b/packages/core-internal/src/shared/stdio.ts @@ -0,0 +1,62 @@ +import type { JSONRPCMessage } from '../types/index'; +import { JSONRPCMessageSchema } from '../types/index'; + +export const STDIO_DEFAULT_MAX_BUFFER_SIZE = 10 * 1024 * 1024; + +/** + * Buffers a continuous stdio stream into discrete JSON-RPC messages. + */ +export class ReadBuffer { + private _buffer?: Buffer; + private _maxBufferSize: number; + + constructor(options?: { maxBufferSize?: number }) { + this._maxBufferSize = options?.maxBufferSize ?? STDIO_DEFAULT_MAX_BUFFER_SIZE; + } + + append(chunk: Buffer): void { + const newSize = (this._buffer?.length ?? 0) + chunk.length; + if (newSize > this._maxBufferSize) { + this.clear(); + throw new Error(`ReadBuffer exceeded maximum size of ${this._maxBufferSize} bytes`); + } + this._buffer = this._buffer ? Buffer.concat([this._buffer, chunk]) : chunk; + } + + readMessage(): JSONRPCMessage | null { + while (this._buffer) { + const index = this._buffer.indexOf('\n'); + if (index === -1) { + return null; + } + + const line = this._buffer.toString('utf8', 0, index).replace(/\r$/, ''); + this._buffer = this._buffer.subarray(index + 1); + + try { + return deserializeMessage(line); + } catch (error) { + // Skip non-JSON lines (e.g., debug output from hot-reload tools like + // tsx or nodemon that write to stdout). Schema validation errors still + // throw so malformed-but-valid-JSON messages surface via onerror. + if (error instanceof SyntaxError) { + continue; + } + throw error; + } + } + return null; + } + + clear(): void { + this._buffer = undefined; + } +} + +export function deserializeMessage(line: string): JSONRPCMessage { + return JSONRPCMessageSchema.parse(JSON.parse(line)); +} + +export function serializeMessage(message: JSONRPCMessage): string { + return JSON.stringify(message) + '\n'; +} diff --git a/packages/core-internal/src/shared/toolNameValidation.ts b/packages/core-internal/src/shared/toolNameValidation.ts new file mode 100644 index 0000000000..41bc44953d --- /dev/null +++ b/packages/core-internal/src/shared/toolNameValidation.ts @@ -0,0 +1,116 @@ +/** + * Tool name validation utilities according to SEP: Specify Format for Tool Names + * + * Tool names SHOULD be between 1 and 128 characters in length (inclusive). + * Tool names are case-sensitive. + * Allowed characters: uppercase and lowercase ASCII letters (`A-Z`, `a-z`), digits + * (`0-9`), underscore (`_`), dash (`-`), and dot (`.`). + * Tool names SHOULD NOT contain spaces, commas, or other special characters. + * + * @see {@link https://github.com/modelcontextprotocol/modelcontextprotocol/issues/986 | SEP-986: Specify Format for Tool Names} + */ + +/** + * Regular expression for valid tool names according to SEP-986 specification + */ +const TOOL_NAME_REGEX = /^[A-Za-z0-9._-]{1,128}$/; + +/** + * Validates a tool name according to the SEP specification + * @param name - The tool name to validate + * @returns An object containing validation result and any warnings + */ +export function validateToolName(name: string): { + isValid: boolean; + warnings: string[]; +} { + const warnings: string[] = []; + + // Check length + if (name.length === 0) { + return { + isValid: false, + warnings: ['Tool name cannot be empty'] + }; + } + + if (name.length > 128) { + return { + isValid: false, + warnings: [`Tool name exceeds maximum length of 128 characters (current: ${name.length})`] + }; + } + + // Check for specific problematic patterns (these are warnings, not validation failures) + if (name.includes(' ')) { + warnings.push('Tool name contains spaces, which may cause parsing issues'); + } + + if (name.includes(',')) { + warnings.push('Tool name contains commas, which may cause parsing issues'); + } + + // Check for potentially confusing patterns (leading/trailing dashes, dots, slashes) + if (name.startsWith('-') || name.endsWith('-')) { + warnings.push('Tool name starts or ends with a dash, which may cause parsing issues in some contexts'); + } + + if (name.startsWith('.') || name.endsWith('.')) { + warnings.push('Tool name starts or ends with a dot, which may cause parsing issues in some contexts'); + } + + // Check for invalid characters + if (!TOOL_NAME_REGEX.test(name)) { + const invalidChars = [...name] + .filter(char => !/[A-Za-z0-9._-]/.test(char)) + .filter((char, index, arr) => arr.indexOf(char) === index); // Remove duplicates + + warnings.push( + `Tool name contains invalid characters: ${invalidChars.map(c => `"${c}"`).join(', ')}`, + 'Allowed characters are: A-Z, a-z, 0-9, underscore (_), dash (-), and dot (.)' + ); + + return { + isValid: false, + warnings + }; + } + + return { + isValid: true, + warnings + }; +} + +/** + * Issues warnings for non-conforming tool names + * @param name - The tool name that triggered the warnings + * @param warnings - Array of warning messages + */ +export function issueToolNameWarning(name: string, warnings: string[]): void { + if (warnings.length > 0) { + console.warn(`Tool name validation warning for "${name}":`); + for (const warning of warnings) { + console.warn(` - ${warning}`); + } + console.warn('Tool registration will proceed, but this may cause compatibility issues.'); + console.warn('Consider updating the tool name to conform to the MCP tool naming standard.'); + console.warn( + 'See SEP: Specify Format for Tool Names (https://github.com/modelcontextprotocol/modelcontextprotocol/issues/986) for more details.' + ); + } +} + +/** + * Validates a tool name and issues warnings for non-conforming names + * @param name - The tool name to validate + * @returns `true` if the name is valid, `false` otherwise + */ +export function validateAndWarnToolName(name: string): boolean { + const result = validateToolName(name); + + // Always issue warnings for any validation issues (both invalid names and warnings) + issueToolNameWarning(name, result.warnings); + + return result.isValid; +} diff --git a/packages/core-internal/src/shared/transport.ts b/packages/core-internal/src/shared/transport.ts new file mode 100644 index 0000000000..ddca2f7992 --- /dev/null +++ b/packages/core-internal/src/shared/transport.ts @@ -0,0 +1,134 @@ +import type { JSONRPCMessage, MessageExtraInfo, RequestId } from '../types/index'; + +export type FetchLike = (url: string | URL, init?: RequestInit) => Promise; + +/** + * Normalizes `HeadersInit` to a plain `Record` for manipulation. + * Handles `Headers` objects, arrays of tuples, and plain objects. + */ +export function normalizeHeaders(headers: RequestInit['headers'] | undefined): Record { + if (!headers) return {}; + + if (headers instanceof Headers) { + return Object.fromEntries(headers.entries()); + } + + if (Array.isArray(headers)) { + return Object.fromEntries(headers); + } + + return { ...(headers as Record) }; +} + +/** + * Creates a fetch function that includes base `RequestInit` options. + * This ensures requests inherit settings like credentials, mode, headers, etc. from the base init. + * + * @param baseFetch - The base fetch function to wrap (defaults to global `fetch`) + * @param baseInit - The base `RequestInit` to merge with each request + * @returns A wrapped fetch function that merges base options with call-specific options + */ +export function createFetchWithInit(baseFetch: FetchLike = fetch, baseInit?: RequestInit): FetchLike { + if (!baseInit) { + return baseFetch; + } + + // Return a wrapped fetch that merges base RequestInit with call-specific init + return async (url: string | URL, init?: RequestInit): Promise => { + const mergedInit: RequestInit = { + ...baseInit, + ...init, + // Headers need special handling - merge instead of replace + headers: init?.headers ? { ...normalizeHeaders(baseInit.headers), ...normalizeHeaders(init.headers) } : baseInit.headers + }; + return baseFetch(url, mergedInit); + }; +} + +/** + * Options for sending a JSON-RPC message. + */ +export type TransportSendOptions = { + /** + * If present, `relatedRequestId` is used to indicate to the transport which incoming request to associate this outgoing message with. + */ + relatedRequestId?: RequestId | undefined; + + /** + * The resumption token used to continue long-running requests that were interrupted. + * + * This allows clients to reconnect and continue from where they left off, if supported by the transport. + */ + resumptionToken?: string | undefined; + + /** + * A callback that is invoked when the resumption token changes, if supported by the transport. + * + * This allows clients to persist the latest token for potential reconnection. + */ + onresumptiontoken?: ((token: string) => void) | undefined; +}; +/** + * Describes the minimal contract for an MCP transport that a client or server can communicate over. + */ +export interface Transport { + /** + * Starts processing messages on the transport, including any connection steps that might need to be taken. + * + * This method should only be called after callbacks are installed, or else messages may be lost. + * + * NOTE: This method should not be called explicitly when using {@linkcode @modelcontextprotocol/client!client/client.Client | Client} or {@linkcode @modelcontextprotocol/server!server/server.Server | Server} classes, as they will implicitly call {@linkcode Transport.start | start()}. + */ + start(): Promise; + + /** + * Sends a JSON-RPC message (request or response). + * + * If present, `relatedRequestId` is used to indicate to the transport which incoming request to associate this outgoing message with. + */ + send(message: JSONRPCMessage, options?: TransportSendOptions): Promise; + + /** + * Closes the connection. + */ + close(): Promise; + + /** + * Callback for when the connection is closed for any reason. + * + * This should be invoked when {@linkcode Transport.close | close()} is called as well. + */ + onclose?: (() => void) | undefined; + + /** + * Callback for when an error occurs. + * + * Note that errors are not necessarily fatal; they are used for reporting any kind of exceptional condition out of band. + */ + onerror?: ((error: Error) => void) | undefined; + + /** + * Callback for when a message (request or response) is received over the connection. + * + * Includes the {@linkcode MessageExtraInfo.request | request} and {@linkcode MessageExtraInfo.authInfo | authInfo} if the transport is authenticated. + * + * The {@linkcode MessageExtraInfo.request | request} can be used to get the original request information (headers, etc.) + */ + onmessage?: ((message: T, extra?: MessageExtraInfo) => void) | undefined; + + /** + * The session ID generated for this connection. + */ + sessionId?: string | undefined; + + /** + * Sets the protocol version used for the connection (called when the initialize response is received). + */ + setProtocolVersion?: ((version: string) => void) | undefined; + + /** + * Sets the supported protocol versions for header validation (called during connect). + * This allows the server to pass its supported versions to the transport. + */ + setSupportedProtocolVersions?: ((versions: string[]) => void) | undefined; +} diff --git a/packages/core-internal/src/shared/uriTemplate.ts b/packages/core-internal/src/shared/uriTemplate.ts new file mode 100644 index 0000000000..5ffe213acd --- /dev/null +++ b/packages/core-internal/src/shared/uriTemplate.ts @@ -0,0 +1,290 @@ +// Claude-authored implementation of RFC 6570 URI Templates + +export type Variables = Record; + +const MAX_TEMPLATE_LENGTH = 1_000_000; // 1MB +const MAX_VARIABLE_LENGTH = 1_000_000; // 1MB +const MAX_TEMPLATE_EXPRESSIONS = 10_000; +const MAX_REGEX_LENGTH = 1_000_000; // 1MB + +export class UriTemplate { + /** + * Returns true if the given string contains any URI template expressions. + * A template expression is a sequence of characters enclosed in curly braces, + * like `{foo}` or `{?bar}`. + */ + static isTemplate(str: string): boolean { + // Look for any sequence of characters between curly braces + // that isn't just whitespace + return /\{[^}\s]+\}/.test(str); + } + + private static validateLength(str: string, max: number, context: string): void { + if (str.length > max) { + throw new Error(`${context} exceeds maximum length of ${max} characters (got ${str.length})`); + } + } + private readonly template: string; + private readonly parts: Array; + + get variableNames(): string[] { + return this.parts.flatMap(part => (typeof part === 'string' ? [] : part.names)); + } + + constructor(template: string) { + UriTemplate.validateLength(template, MAX_TEMPLATE_LENGTH, 'Template'); + this.template = template; + this.parts = this.parse(template); + } + + toString(): string { + return this.template; + } + + private parse(template: string): Array { + const parts: Array = []; + let currentText = ''; + let i = 0; + let expressionCount = 0; + + while (i < template.length) { + if (template[i] === '{') { + if (currentText) { + parts.push(currentText); + currentText = ''; + } + const end = template.indexOf('}', i); + if (end === -1) throw new Error('Unclosed template expression'); + + expressionCount++; + if (expressionCount > MAX_TEMPLATE_EXPRESSIONS) { + throw new Error(`Template contains too many expressions (max ${MAX_TEMPLATE_EXPRESSIONS})`); + } + + const expr = template.slice(i + 1, end); + const operator = this.getOperator(expr); + const exploded = expr.includes('*'); + const names = this.getNames(expr); + const name = names[0]!; + + // Validate variable name length + for (const name of names) { + UriTemplate.validateLength(name, MAX_VARIABLE_LENGTH, 'Variable name'); + } + + parts.push({ name, operator, names, exploded }); + i = end + 1; + } else { + currentText += template[i]; + i++; + } + } + + if (currentText) { + parts.push(currentText); + } + + return parts; + } + + private getOperator(expr: string): string { + const operators = ['+', '#', '.', '/', '?', '&']; + return operators.find(op => expr.startsWith(op)) || ''; + } + + private getNames(expr: string): string[] { + const operator = this.getOperator(expr); + return expr + .slice(operator.length) + .split(',') + .map(name => name.replace('*', '').trim()) + .filter(name => name.length > 0); + } + + private encodeValue(value: string, operator: string): string { + UriTemplate.validateLength(value, MAX_VARIABLE_LENGTH, 'Variable value'); + if (operator === '+' || operator === '#') { + return encodeURI(value); + } + return encodeURIComponent(value); + } + + private expandPart( + part: { + name: string; + operator: string; + names: string[]; + exploded: boolean; + }, + variables: Variables + ): string { + if (part.operator === '?' || part.operator === '&') { + const pairs = part.names + .map(name => { + const value = variables[name]; + if (value === undefined) return ''; + const encoded = Array.isArray(value) + ? value.map(v => this.encodeValue(v, part.operator)).join(',') + : this.encodeValue(value.toString(), part.operator); + return `${name}=${encoded}`; + }) + .filter(pair => pair.length > 0); + + if (pairs.length === 0) return ''; + const separator = part.operator === '?' ? '?' : '&'; + return separator + pairs.join('&'); + } + + if (part.names.length > 1) { + const values = part.names.map(name => variables[name]).filter(v => v !== undefined); + if (values.length === 0) return ''; + return values.map(v => (Array.isArray(v) ? v[0] : v)).join(','); + } + + const value = variables[part.name]; + if (value === undefined) return ''; + + const values = Array.isArray(value) ? value : [value]; + const encoded = values.map(v => this.encodeValue(v, part.operator)); + + switch (part.operator) { + case '': { + return encoded.join(','); + } + case '+': { + return encoded.join(','); + } + case '#': { + return '#' + encoded.join(','); + } + case '.': { + return '.' + encoded.join('.'); + } + case '/': { + return '/' + encoded.join('/'); + } + default: { + return encoded.join(','); + } + } + } + + expand(variables: Variables): string { + let result = ''; + let hasQueryParam = false; + + for (const part of this.parts) { + if (typeof part === 'string') { + result += part; + continue; + } + + const expanded = this.expandPart(part, variables); + if (!expanded) continue; + + // Convert ? to & if we already have a query parameter + result += (part.operator === '?' || part.operator === '&') && hasQueryParam ? expanded.replace('?', '&') : expanded; + + if (part.operator === '?' || part.operator === '&') { + hasQueryParam = true; + } + } + + return result; + } + + private escapeRegExp(str: string): string { + return str.replaceAll(/[.*+?^${}()|[\]\\]/g, String.raw`\$&`); + } + + private partToRegExp(part: { + name: string; + operator: string; + names: string[]; + exploded: boolean; + }): Array<{ pattern: string; name: string }> { + const patterns: Array<{ pattern: string; name: string }> = []; + + // Validate variable name length for matching + for (const name of part.names) { + UriTemplate.validateLength(name, MAX_VARIABLE_LENGTH, 'Variable name'); + } + + if (part.operator === '?' || part.operator === '&') { + for (let i = 0; i < part.names.length; i++) { + const name = part.names[i]!; + const prefix = i === 0 ? '\\' + part.operator : '&'; + patterns.push({ + pattern: prefix + this.escapeRegExp(name) + '=([^&]+)', + name + }); + } + return patterns; + } + + let pattern: string; + const name = part.name; + + switch (part.operator) { + case '': { + pattern = part.exploded ? '([^/,]+(?:,[^/,]+)*)' : '([^/,]+)'; + break; + } + case '+': + case '#': { + pattern = '(.+)'; + break; + } + case '.': { + pattern = String.raw`\.([^/,]+)`; + break; + } + case '/': { + pattern = '/' + (part.exploded ? '([^/,]+(?:,[^/,]+)*)' : '([^/,]+)'); + break; + } + default: { + pattern = '([^/]+)'; + } + } + + patterns.push({ pattern, name }); + return patterns; + } + + match(uri: string): Variables | null { + UriTemplate.validateLength(uri, MAX_TEMPLATE_LENGTH, 'URI'); + let pattern = '^'; + const names: Array<{ name: string; exploded: boolean }> = []; + + for (const part of this.parts) { + if (typeof part === 'string') { + pattern += this.escapeRegExp(part); + } else { + const patterns = this.partToRegExp(part); + for (const { pattern: partPattern, name } of patterns) { + pattern += partPattern; + names.push({ name, exploded: part.exploded }); + } + } + } + + pattern += '$'; + UriTemplate.validateLength(pattern, MAX_REGEX_LENGTH, 'Generated regex pattern'); + const regex = new RegExp(pattern); + const match = uri.match(regex); + + if (!match) return null; + + const result: Variables = {}; + for (const [i, name_] of names.entries()) { + const { name, exploded } = name_!; + const value = match[i + 1]!; + const cleanName = name.replace('*', ''); + + result[cleanName] = exploded && value.includes(',') ? value.split(',') : value; + } + + return result; + } +} diff --git a/packages/core-internal/src/types/constants.ts b/packages/core-internal/src/types/constants.ts new file mode 100644 index 0000000000..018f9ecb51 --- /dev/null +++ b/packages/core-internal/src/types/constants.ts @@ -0,0 +1,86 @@ +export const LATEST_PROTOCOL_VERSION = '2025-11-25'; +export const DEFAULT_NEGOTIATED_PROTOCOL_VERSION = '2025-03-26'; +export const SUPPORTED_PROTOCOL_VERSIONS = [LATEST_PROTOCOL_VERSION, '2025-06-18', '2025-03-26', '2024-11-05', '2024-10-07']; + +export const RELATED_TASK_META_KEY = 'io.modelcontextprotocol/related-task'; + +/* Reserved `_meta` keys for the per-request envelope (protocol revision 2026-07-28) */ + +/** + * `_meta` key carrying the MCP protocol version governing a request. + * + * For the HTTP transport, the value must match the `MCP-Protocol-Version` header. + */ +export const PROTOCOL_VERSION_META_KEY = 'io.modelcontextprotocol/protocolVersion'; + +/** + * `_meta` key identifying the client software making a request. + */ +export const CLIENT_INFO_META_KEY = 'io.modelcontextprotocol/clientInfo'; + +/** + * `_meta` key carrying the client's capabilities for a request. + * + * Capabilities are declared per request rather than once at initialization; + * servers must not infer capabilities from prior requests. + */ +export const CLIENT_CAPABILITIES_META_KEY = 'io.modelcontextprotocol/clientCapabilities'; + +/** + * `_meta` key carrying the desired log level for a request. + * + * When absent, the server must not send `notifications/message` notifications + * for the request. + * + * @deprecated Deprecated as of protocol version 2026-07-28 (SEP-2577); remains + * in the specification for at least twelve months. + */ +export const LOG_LEVEL_META_KEY = 'io.modelcontextprotocol/logLevel'; + +/* + * Reserved `_meta` keys for distributed trace context propagation (SEP-414). + * + * These unprefixed keys are reserved by the MCP specification as an explicit + * exception to the `_meta` key prefix rule. The SDK does not interpret them; + * they pass through `_meta` untouched for OpenTelemetry-style propagation. + */ + +/** + * `_meta` key carrying W3C Trace Context for distributed tracing (SEP-414). + * + * When present, the value MUST follow the W3C `traceparent` header format, + * e.g. `00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01`. + * + * @see https://www.w3.org/TR/trace-context/#traceparent-header + */ +export const TRACEPARENT_META_KEY = 'traceparent'; + +/** + * `_meta` key carrying vendor-specific trace state for distributed tracing (SEP-414). + * + * When present, the value MUST follow the W3C `tracestate` header format, + * e.g. `vendor1=value1,vendor2=value2`. + * + * @see https://www.w3.org/TR/trace-context/#tracestate-header + */ +export const TRACESTATE_META_KEY = 'tracestate'; + +/** + * `_meta` key carrying cross-cutting propagation values for distributed tracing (SEP-414). + * + * When present, the value MUST follow the W3C Baggage header format, + * e.g. `userId=alice,serverRegion=us-east-1`. + * + * @see https://www.w3.org/TR/baggage/ + */ +export const BAGGAGE_META_KEY = 'baggage'; + +/* JSON-RPC types */ +export const JSONRPC_VERSION = '2.0'; + +/* Standard JSON-RPC error code constants */ +export const PARSE_ERROR = -32_700; +export const INVALID_REQUEST = -32_600; +export const METHOD_NOT_FOUND = -32_601; +export const INVALID_PARAMS = -32_602; +export const INTERNAL_ERROR = -32_603; diff --git a/packages/core-internal/src/types/enums.ts b/packages/core-internal/src/types/enums.ts new file mode 100644 index 0000000000..0e3b65f9f0 --- /dev/null +++ b/packages/core-internal/src/types/enums.ts @@ -0,0 +1,26 @@ +/** + * Error codes for protocol errors that cross the wire as JSON-RPC error responses. + * These follow the JSON-RPC specification and MCP-specific extensions. + */ +export enum ProtocolErrorCode { + // Standard JSON-RPC error codes + ParseError = -32_700, + InvalidRequest = -32_600, + MethodNotFound = -32_601, + InvalidParams = -32_602, + InternalError = -32_603, + + // MCP-specific error codes + ResourceNotFound = -32_002, + /** + * Processing the request requires a capability the client did not declare + * in the request's `clientCapabilities` (protocol revision 2026-07-28). + */ + MissingRequiredClientCapability = -32_003, + /** + * The request's protocol version is unknown to the server or unsupported + * by it (protocol revision 2026-07-28). + */ + UnsupportedProtocolVersion = -32_004, + UrlElicitationRequired = -32_042 +} diff --git a/packages/core-internal/src/types/errors.ts b/packages/core-internal/src/types/errors.ts new file mode 100644 index 0000000000..3d3654d46f --- /dev/null +++ b/packages/core-internal/src/types/errors.ts @@ -0,0 +1,85 @@ +import { ProtocolErrorCode } from './enums'; +import type { ElicitRequestURLParams, UnsupportedProtocolVersionErrorData } from './types'; + +/** + * Protocol errors are JSON-RPC errors that cross the wire as error responses. + * They use numeric error codes from the {@linkcode ProtocolErrorCode} enum. + */ +export class ProtocolError extends Error { + constructor( + public readonly code: number, + message: string, + public readonly data?: unknown + ) { + super(message); + this.name = 'ProtocolError'; + } + + /** + * Factory method to create the appropriate error type based on the error code and data + */ + static fromError(code: number, message: string, data?: unknown): ProtocolError { + // Check for specific error types + if (code === ProtocolErrorCode.UrlElicitationRequired && data) { + const errorData = data as { elicitations?: unknown[] }; + if (errorData.elicitations) { + return new UrlElicitationRequiredError(errorData.elicitations as ElicitRequestURLParams[], message); + } + } + + if (code === ProtocolErrorCode.UnsupportedProtocolVersion && data) { + const errorData = data as Partial; + if (Array.isArray(errorData.supported) && typeof errorData.requested === 'string') { + return new UnsupportedProtocolVersionError({ supported: errorData.supported, requested: errorData.requested }, message); + } + } + + // Default to generic ProtocolError + return new ProtocolError(code, message, data); + } +} + +/** + * Specialized error type when a tool requires a URL mode elicitation. + * This makes it nicer for the client to handle since there is specific data to work with instead of just a code to check against. + */ +export class UrlElicitationRequiredError extends ProtocolError { + constructor(elicitations: ElicitRequestURLParams[], message: string = `URL elicitation${elicitations.length > 1 ? 's' : ''} required`) { + super(ProtocolErrorCode.UrlElicitationRequired, message, { + elicitations: elicitations + }); + } + + get elicitations(): ElicitRequestURLParams[] { + return (this.data as { elicitations: ElicitRequestURLParams[] })?.elicitations ?? []; + } +} + +/** + * Error type for the `-32004` UnsupportedProtocolVersion protocol error (protocol + * revision 2026-07-28): the request's protocol version is unknown to the server or + * unsupported by it. + * + * The error data lists the protocol versions the receiver supports (`supported`), + * so the sender can choose a mutually supported version and retry, and echoes the + * version that was requested (`requested`). + */ +export class UnsupportedProtocolVersionError extends ProtocolError { + constructor(data: UnsupportedProtocolVersionErrorData, message: string = `Unsupported protocol version: ${data.requested}`) { + super(ProtocolErrorCode.UnsupportedProtocolVersion, message, data); + } + + /** + * Protocol versions the receiver supports. + */ + get supported(): string[] { + return (this.data as UnsupportedProtocolVersionErrorData).supported; + } + + /** + * The protocol version that was requested. + */ + get requested(): string { + return (this.data as UnsupportedProtocolVersionErrorData).requested; + } +} diff --git a/packages/core-internal/src/types/guards.ts b/packages/core-internal/src/types/guards.ts new file mode 100644 index 0000000000..3bd069b0d8 --- /dev/null +++ b/packages/core-internal/src/types/guards.ts @@ -0,0 +1,110 @@ +import { + CallToolResultSchema, + InitializedNotificationSchema, + InitializeRequestSchema, + JSONRPCErrorResponseSchema, + JSONRPCMessageSchema, + JSONRPCNotificationSchema, + JSONRPCRequestSchema, + JSONRPCResponseSchema, + JSONRPCResultResponseSchema, + TaskAugmentedRequestParamsSchema +} from './schemas'; +import type { + CallToolResult, + CompleteRequest, + CompleteRequestPrompt, + CompleteRequestResourceTemplate, + InitializedNotification, + InitializeRequest, + JSONRPCErrorResponse, + JSONRPCMessage, + JSONRPCNotification, + JSONRPCRequest, + JSONRPCResponse, + JSONRPCResultResponse, + TaskAugmentedRequestParams +} from './types'; + +/** + * Validates and parses an unknown value as a JSON-RPC message. + * + * Use this to validate incoming messages in custom transport implementations. + * Throws if the value does not conform to the JSON-RPC message schema. + * + * @param value - The value to validate (typically a parsed JSON object). + * @returns The validated {@linkcode JSONRPCMessage}. + * @throws If validation fails. + */ +export function parseJSONRPCMessage(value: unknown): JSONRPCMessage { + return JSONRPCMessageSchema.parse(value); +} + +export const isJSONRPCRequest = (value: unknown): value is JSONRPCRequest => JSONRPCRequestSchema.safeParse(value).success; + +export const isJSONRPCNotification = (value: unknown): value is JSONRPCNotification => JSONRPCNotificationSchema.safeParse(value).success; + +/** + * Checks if a value is a valid {@linkcode JSONRPCResultResponse}. + * @param value - The value to check. + * + * @returns True if the value is a valid {@linkcode JSONRPCResultResponse}, false otherwise. + */ +export const isJSONRPCResultResponse = (value: unknown): value is JSONRPCResultResponse => + JSONRPCResultResponseSchema.safeParse(value).success; + +/** + * Checks if a value is a valid {@linkcode JSONRPCErrorResponse}. + * @param value - The value to check. + * + * @returns True if the value is a valid {@linkcode JSONRPCErrorResponse}, false otherwise. + */ +export const isJSONRPCErrorResponse = (value: unknown): value is JSONRPCErrorResponse => + JSONRPCErrorResponseSchema.safeParse(value).success; + +/** + * Checks if a value is a valid {@linkcode JSONRPCResponse} (either a result or error response). + * @param value - The value to check. + * + * @returns True if the value is a valid {@linkcode JSONRPCResponse}, false otherwise. + */ +export const isJSONRPCResponse = (value: unknown): value is JSONRPCResponse => JSONRPCResponseSchema.safeParse(value).success; + +/** + * Checks if a value is a valid {@linkcode CallToolResult}. + * @param value - The value to check. + * + * @returns True if the value is a valid {@linkcode CallToolResult}, false otherwise. + */ +export const isCallToolResult = (value: unknown): value is CallToolResult => { + if (typeof value !== 'object' || value === null || !('content' in value)) return false; + return CallToolResultSchema.safeParse(value).success; +}; + +/** + * Checks if a value is a valid {@linkcode TaskAugmentedRequestParams}. + * @param value - The value to check. + * + * @returns True if the value is a valid {@linkcode TaskAugmentedRequestParams}, false otherwise. + */ +export const isTaskAugmentedRequestParams = (value: unknown): value is TaskAugmentedRequestParams => + TaskAugmentedRequestParamsSchema.safeParse(value).success; + +export const isInitializeRequest = (value: unknown): value is InitializeRequest => InitializeRequestSchema.safeParse(value).success; + +export const isInitializedNotification = (value: unknown): value is InitializedNotification => + InitializedNotificationSchema.safeParse(value).success; + +export function assertCompleteRequestPrompt(request: CompleteRequest): asserts request is CompleteRequestPrompt { + if (request.params.ref.type !== 'ref/prompt') { + throw new TypeError(`Expected CompleteRequestPrompt, but got ${request.params.ref.type}`); + } + void (request as CompleteRequestPrompt); +} + +export function assertCompleteRequestResourceTemplate(request: CompleteRequest): asserts request is CompleteRequestResourceTemplate { + if (request.params.ref.type !== 'ref/resource') { + throw new TypeError(`Expected CompleteRequestResourceTemplate, but got ${request.params.ref.type}`); + } + void (request as CompleteRequestResourceTemplate); +} diff --git a/packages/core-internal/src/types/index.ts b/packages/core-internal/src/types/index.ts new file mode 100644 index 0000000000..46dd118f11 --- /dev/null +++ b/packages/core-internal/src/types/index.ts @@ -0,0 +1,9 @@ +// Internal barrel — re-exports everything for use within the SDK packages. +// The public API is defined in @modelcontextprotocol/core-internal/public (see exports/public/index.ts). +export * from './constants'; +export * from './enums'; +export * from './errors'; +export * from './guards'; +export * from './schemas'; +export * from './specTypeSchema'; +export * from './types'; diff --git a/packages/core-internal/src/types/schemas.ts b/packages/core-internal/src/types/schemas.ts new file mode 100644 index 0000000000..3c942256fd --- /dev/null +++ b/packages/core-internal/src/types/schemas.ts @@ -0,0 +1,2346 @@ +import * as z from 'zod/v4'; + +import { + CLIENT_CAPABILITIES_META_KEY, + CLIENT_INFO_META_KEY, + JSONRPC_VERSION, + LOG_LEVEL_META_KEY, + PROTOCOL_VERSION_META_KEY, + RELATED_TASK_META_KEY +} from './constants'; +import type { + JSONArray, + JSONObject, + JSONValue, + NotificationMethod, + NotificationTypeMap, + RequestMethod, + RequestTypeMap, + ResultTypeMap +} from './types'; + +export const JSONValueSchema: z.ZodType = z.lazy(() => + z.union([z.string(), z.number(), z.boolean(), z.null(), z.record(z.string(), JSONValueSchema), z.array(JSONValueSchema)]) +); +export const JSONObjectSchema: z.ZodType = z.record(z.string(), JSONValueSchema); +export const JSONArraySchema: z.ZodType = z.array(JSONValueSchema); +/** + * A progress token, used to associate progress notifications with the original request. + */ +export const ProgressTokenSchema = z.union([z.string(), z.number().int()]); + +/** + * An opaque token used to represent a cursor for pagination. + */ +export const CursorSchema = z.string(); + +/** + * Task creation parameters, used to ask that the server create a task to represent a request. + */ +export const TaskCreationParamsSchema = z.looseObject({ + /** + * Requested duration in milliseconds to retain task from creation. + */ + ttl: z.number().optional(), + + /** + * Time in milliseconds to wait between task status requests. + */ + pollInterval: z.number().optional() +}); + +export const TaskMetadataSchema = z.object({ + ttl: z.number().optional() +}); + +/** + * Metadata for associating messages with a task. + * Include this in the `_meta` field under the key `io.modelcontextprotocol/related-task`. + */ +export const RelatedTaskMetadataSchema = z.object({ + taskId: z.string() +}); + +export const RequestMetaSchema = z.looseObject({ + /** + * If specified, the caller is requesting out-of-band progress notifications for this request (as represented by notifications/progress). The value of this parameter is an opaque token that will be attached to any subsequent notifications. The receiver is not obligated to provide these notifications. + */ + progressToken: ProgressTokenSchema.optional(), + /** + * If specified, this request is related to the provided task. + */ + [RELATED_TASK_META_KEY]: RelatedTaskMetadataSchema.optional() +}); + +/** + * Common params for any request. + */ +export const BaseRequestParamsSchema = z.object({ + /** + * See [General fields: `_meta`](/specification/draft/basic/index#meta) for notes on `_meta` usage. + */ + _meta: RequestMetaSchema.optional() +}); + +/** + * Common params for any task-augmented request. + */ +export const TaskAugmentedRequestParamsSchema = BaseRequestParamsSchema.extend({ + /** + * If specified, the caller is requesting task-augmented execution for this request. + * The request will return a `CreateTaskResult` immediately, and the actual result can be + * retrieved later via `tasks/result`. + * + * Task augmentation is subject to capability negotiation - receivers MUST declare support + * for task augmentation of specific request types in their capabilities. + */ + task: TaskMetadataSchema.optional() +}); + +export const RequestSchema = z.object({ + method: z.string(), + params: BaseRequestParamsSchema.loose().optional() +}); + +export const NotificationsParamsSchema = z.object({ + /** + * See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields) + * for notes on `_meta` usage. + */ + _meta: RequestMetaSchema.optional() +}); + +export const NotificationSchema = z.object({ + method: z.string(), + params: NotificationsParamsSchema.loose().optional() +}); + +export const ResultSchema = z.looseObject({ + /** + * See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields) + * for notes on `_meta` usage. + */ + _meta: RequestMetaSchema.optional(), + /** + * Indicates the type of the result, allowing the receiver to determine how to + * parse the result object. Servers implementing protocol revision 2026-07-28 or + * later always include this field; results from earlier revisions omit it, and + * an absent value must be treated as `"complete"`. + */ + resultType: z.string().optional() +}); + +/** + * A uniquely identifying ID for a request in JSON-RPC. + */ +export const RequestIdSchema = z.union([z.string(), z.number().int()]); + +/** + * A request that expects a response. + */ +export const JSONRPCRequestSchema = z + .object({ + jsonrpc: z.literal(JSONRPC_VERSION), + id: RequestIdSchema, + ...RequestSchema.shape + }) + .strict(); + +/** + * A notification which does not expect a response. + */ +export const JSONRPCNotificationSchema = z + .object({ + jsonrpc: z.literal(JSONRPC_VERSION), + ...NotificationSchema.shape + }) + .strict(); + +/** + * A successful (non-error) response to a request. + */ +export const JSONRPCResultResponseSchema = z + .object({ + jsonrpc: z.literal(JSONRPC_VERSION), + id: RequestIdSchema, + result: ResultSchema + }) + .strict(); + +/** + * A response to a request that indicates an error occurred. + */ +export const JSONRPCErrorResponseSchema = z + .object({ + jsonrpc: z.literal(JSONRPC_VERSION), + id: RequestIdSchema.optional(), + error: z.object({ + /** + * The error type that occurred. + */ + code: z.number().int(), + /** + * A short description of the error. The message SHOULD be limited to a concise single sentence. + */ + message: z.string(), + /** + * Additional information about the error. The value of this member is defined by the sender (e.g. detailed error information, nested errors etc.). + */ + data: z.unknown().optional() + }) + }) + .strict(); + +export const JSONRPCMessageSchema = z.union([ + JSONRPCRequestSchema, + JSONRPCNotificationSchema, + JSONRPCResultResponseSchema, + JSONRPCErrorResponseSchema +]); + +export const JSONRPCResponseSchema = z.union([JSONRPCResultResponseSchema, JSONRPCErrorResponseSchema]); + +/* Empty result */ +/** + * A response that indicates success but carries no data. + */ +export const EmptyResultSchema = ResultSchema.strict(); + +export const CancelledNotificationParamsSchema = NotificationsParamsSchema.extend({ + /** + * The ID of the request to cancel. + * + * This MUST correspond to the ID of a request previously issued in the same direction. + */ + requestId: RequestIdSchema.optional(), + /** + * An optional string describing the reason for the cancellation. This MAY be logged or presented to the user. + */ + reason: z.string().optional() +}); +/* Cancellation */ +/** + * This notification can be sent by either side to indicate that it is cancelling a previously-issued request. + * + * The request SHOULD still be in-flight, but due to communication latency, it is always possible that this notification MAY arrive after the request has already finished. + * + * This notification indicates that the result will be unused, so any associated processing SHOULD cease. + * + * A client MUST NOT attempt to cancel its {@linkcode InitializeRequest | initialize} request. + */ +export const CancelledNotificationSchema = NotificationSchema.extend({ + method: z.literal('notifications/cancelled'), + params: CancelledNotificationParamsSchema +}); + +/* Base Metadata */ +/** + * Icon schema for use in {@link Tool | tools}, {@link Prompt | prompts}, {@link Resource | resources}, and {@link Implementation | implementations}. + */ +export const IconSchema = z.object({ + /** + * URL or data URI for the icon. + */ + src: z.string(), + /** + * Optional MIME type for the icon. + */ + mimeType: z.string().optional(), + /** + * Optional array of strings that specify sizes at which the icon can be used. + * Each string should be in WxH format (e.g., `"48x48"`, `"96x96"`) or `"any"` for scalable formats like SVG. + * + * If not provided, the client should assume that the icon can be used at any size. + */ + sizes: z.array(z.string()).optional(), + /** + * Optional specifier for the theme this icon is designed for. `light` indicates + * the icon is designed to be used with a light background, and `dark` indicates + * the icon is designed to be used with a dark background. + * + * If not provided, the client should assume the icon can be used with any theme. + */ + theme: z.enum(['light', 'dark']).optional() +}); + +/** + * Base schema to add `icons` property. + * + */ +export const IconsSchema = z.object({ + /** + * Optional set of sized icons that the client can display in a user interface. + * + * Clients that support rendering icons MUST support at least the following MIME types: + * - `image/png` - PNG images (safe, universal compatibility) + * - `image/jpeg` (and `image/jpg`) - JPEG images (safe, universal compatibility) + * + * Clients that support rendering icons SHOULD also support: + * - `image/svg+xml` - SVG images (scalable but requires security precautions) + * - `image/webp` - WebP images (modern, efficient format) + */ + icons: z.array(IconSchema).optional() +}); + +/** + * Base metadata interface for common properties across {@link Resource | resources}, {@link Tool | tools}, {@link Prompt | prompts}, and {@link Implementation | implementations}. + */ +export const BaseMetadataSchema = z.object({ + /** Intended for programmatic or logical use, but used as a display name in past specs or fallback */ + name: z.string(), + /** + * Intended for UI and end-user contexts — optimized to be human-readable and easily understood, + * even by those unfamiliar with domain-specific terminology. + * + * If not provided, the `name` should be used for display (except for `Tool`, + * where `annotations.title` should be given precedence over using `name`, + * if present). + */ + title: z.string().optional() +}); + +/* Initialization */ +/** + * Describes the name and version of an MCP implementation. + */ +export const ImplementationSchema = BaseMetadataSchema.extend({ + ...BaseMetadataSchema.shape, + ...IconsSchema.shape, + version: z.string(), + /** + * An optional URL of the website for this implementation. + */ + websiteUrl: z.string().optional(), + + /** + * An optional human-readable description of what this implementation does. + * + * This can be used by clients or servers to provide context about their purpose + * and capabilities. For example, a server might describe the types of resources + * or tools it provides, while a client might describe its intended use case. + */ + description: z.string().optional() +}); + +const FormElicitationCapabilitySchema = z.intersection( + z.object({ + applyDefaults: z.boolean().optional() + }), + JSONObjectSchema +); + +const ElicitationCapabilitySchema = z.preprocess( + value => { + if (value && typeof value === 'object' && !Array.isArray(value) && Object.keys(value as Record).length === 0) { + return { form: {} }; + } + return value; + }, + z.intersection( + z.object({ + form: FormElicitationCapabilitySchema.optional(), + url: JSONObjectSchema.optional() + }), + JSONObjectSchema.optional() + ) +); + +/** + * Task capabilities for clients, indicating which request types support task creation. + */ +export const ClientTasksCapabilitySchema = z.looseObject({ + /** + * Present if the client supports listing tasks. + */ + list: JSONObjectSchema.optional(), + /** + * Present if the client supports cancelling tasks. + */ + cancel: JSONObjectSchema.optional(), + /** + * Capabilities for task creation on specific request types. + */ + requests: z + .looseObject({ + /** + * Task support for sampling requests. + */ + sampling: z + .looseObject({ + createMessage: JSONObjectSchema.optional() + }) + .optional(), + /** + * Task support for elicitation requests. + */ + elicitation: z + .looseObject({ + create: JSONObjectSchema.optional() + }) + .optional() + }) + .optional() +}); + +/** + * Task capabilities for servers, indicating which request types support task creation. + */ +export const ServerTasksCapabilitySchema = z.looseObject({ + /** + * Present if the server supports listing tasks. + */ + list: JSONObjectSchema.optional(), + /** + * Present if the server supports cancelling tasks. + */ + cancel: JSONObjectSchema.optional(), + /** + * Capabilities for task creation on specific request types. + */ + requests: z + .looseObject({ + /** + * Task support for tool requests. + */ + tools: z + .looseObject({ + call: JSONObjectSchema.optional() + }) + .optional() + }) + .optional() +}); + +/** + * Capabilities a client may support. Known capabilities are defined here, in this schema, but this is not a closed set: any client can define its own, additional capabilities. + */ +export const ClientCapabilitiesSchema = z.object({ + /** + * Experimental, non-standard capabilities that the client supports. + */ + experimental: z.record(z.string(), JSONObjectSchema).optional(), + /** + * Present if the client supports sampling from an LLM. + * + * @deprecated Deprecated as of protocol version 2026-07-28 (SEP-2577); remains + * in the specification for at least twelve months. Migrate to calling LLM + * provider APIs directly. + */ + sampling: z + .object({ + /** + * Present if the client supports context inclusion via `includeContext` parameter. + * If not declared, servers SHOULD only use `includeContext: "none"` (or omit it). + */ + context: JSONObjectSchema.optional(), + /** + * Present if the client supports tool use via `tools` and `toolChoice` parameters. + */ + tools: JSONObjectSchema.optional() + }) + .optional(), + /** + * Present if the client supports eliciting user input. + */ + elicitation: ElicitationCapabilitySchema.optional(), + /** + * Present if the client supports listing roots. + * + * @deprecated Deprecated as of protocol version 2026-07-28 (SEP-2577); remains + * in the specification for at least twelve months. Migrate to passing paths via + * tool parameters, resource URIs, or configuration. + */ + roots: z + .object({ + /** + * Whether the client supports issuing notifications for changes to the roots list. + */ + listChanged: z.boolean().optional() + }) + .optional(), + /** + * Present if the client supports task creation. + */ + tasks: ClientTasksCapabilitySchema.optional(), + /** + * Extensions that the client supports. Keys are extension identifiers (vendor-prefix/extension-name). + */ + extensions: z.record(z.string(), JSONObjectSchema).optional() +}); + +export const InitializeRequestParamsSchema = BaseRequestParamsSchema.extend({ + /** + * The latest version of the Model Context Protocol that the client supports. The client MAY decide to support older versions as well. + */ + protocolVersion: z.string(), + capabilities: ClientCapabilitiesSchema, + clientInfo: ImplementationSchema +}); +/** + * This request is sent from the client to the server when it first connects, asking it to begin initialization. + */ +export const InitializeRequestSchema = RequestSchema.extend({ + method: z.literal('initialize'), + params: InitializeRequestParamsSchema +}); + +/** + * Capabilities that a server may support. Known capabilities are defined here, in this schema, but this is not a closed set: any server can define its own, additional capabilities. + */ +export const ServerCapabilitiesSchema = z.object({ + /** + * Experimental, non-standard capabilities that the server supports. + */ + experimental: z.record(z.string(), JSONObjectSchema).optional(), + /** + * Present if the server supports sending log messages to the client. + * + * @deprecated Deprecated as of protocol version 2026-07-28 (SEP-2577); remains + * in the specification for at least twelve months. Migrate to stderr logging + * (STDIO servers) or OpenTelemetry. + */ + logging: JSONObjectSchema.optional(), + /** + * Present if the server supports sending completions to the client. + */ + completions: JSONObjectSchema.optional(), + /** + * Present if the server offers any prompt templates. + */ + prompts: z + .object({ + /** + * Whether this server supports issuing notifications for changes to the prompt list. + */ + listChanged: z.boolean().optional() + }) + .optional(), + /** + * Present if the server offers any resources to read. + */ + resources: z + .object({ + /** + * Whether this server supports clients subscribing to resource updates. + */ + subscribe: z.boolean().optional(), + + /** + * Whether this server supports issuing notifications for changes to the resource list. + */ + listChanged: z.boolean().optional() + }) + .optional(), + /** + * Present if the server offers any tools to call. + */ + tools: z + .object({ + /** + * Whether this server supports issuing notifications for changes to the tool list. + */ + listChanged: z.boolean().optional() + }) + .optional(), + /** + * Present if the server supports task creation. + */ + tasks: ServerTasksCapabilitySchema.optional(), + /** + * Extensions that the server supports. Keys are extension identifiers (vendor-prefix/extension-name). + */ + extensions: z.record(z.string(), JSONObjectSchema).optional() +}); + +/** + * After receiving an initialize request from the client, the server sends this response. + */ +export const InitializeResultSchema = ResultSchema.extend({ + /** + * The version of the Model Context Protocol that the server wants to use. This may not match the version that the client requested. If the client cannot support this version, it MUST disconnect. + */ + protocolVersion: z.string(), + capabilities: ServerCapabilitiesSchema, + serverInfo: ImplementationSchema, + /** + * Instructions describing how to use the server and its features. + * + * This can be used by clients to improve the LLM's understanding of available tools, resources, etc. It can be thought of like a "hint" to the model. For example, this information MAY be added to the system prompt. + */ + instructions: z.string().optional() +}); + +/** + * This notification is sent from the client to the server after initialization has finished. + */ +export const InitializedNotificationSchema = NotificationSchema.extend({ + method: z.literal('notifications/initialized'), + params: NotificationsParamsSchema.optional() +}); + +/* Discovery */ +/** + * A request from the client asking the server to advertise its supported protocol + * versions, capabilities, and other metadata (protocol revision 2026-07-28). Servers + * MUST implement `server/discover`. Clients MAY call it but are not required to — + * version negotiation can also happen inline via the per-request `_meta` envelope. + */ +export const DiscoverRequestSchema = RequestSchema.extend({ + method: z.literal('server/discover'), + params: BaseRequestParamsSchema.optional() +}); + +/** + * The result returned by the server for a `server/discover` request. + */ +export const DiscoverResultSchema = ResultSchema.extend({ + /** + * MCP protocol versions this server supports. The client should choose a + * version from this list for use in subsequent requests. + */ + supportedVersions: z.array(z.string()), + /** + * The capabilities of the server. + */ + capabilities: ServerCapabilitiesSchema, + /** + * Information about the server software implementation. + */ + serverInfo: ImplementationSchema, + /** + * Instructions describing how to use the server and its features. + * + * This can be used by clients to improve the LLM's understanding of available tools, resources, etc. It can be thought of like a "hint" to the model. For example, this information MAY be added to the system prompt. + */ + instructions: z.string().optional() +}); + +/* Ping */ +/** + * A ping, issued by either the server or the client, to check that the other party is still alive. The receiver must promptly respond, or else may be disconnected. + */ +export const PingRequestSchema = RequestSchema.extend({ + method: z.literal('ping'), + params: BaseRequestParamsSchema.optional() +}); + +/* Progress notifications */ +export const ProgressSchema = z.object({ + /** + * The progress thus far. This should increase every time progress is made, even if the total is unknown. + */ + progress: z.number(), + /** + * Total number of items to process (or total progress required), if known. + */ + total: z.optional(z.number()), + /** + * An optional message describing the current progress. + */ + message: z.optional(z.string()) +}); + +export const ProgressNotificationParamsSchema = z.object({ + ...NotificationsParamsSchema.shape, + ...ProgressSchema.shape, + /** + * The progress token which was given in the initial request, used to associate this notification with the request that is proceeding. + */ + progressToken: ProgressTokenSchema +}); +/** + * An out-of-band notification used to inform the receiver of a progress update for a long-running request. + * + * @category notifications/progress + */ +export const ProgressNotificationSchema = NotificationSchema.extend({ + method: z.literal('notifications/progress'), + params: ProgressNotificationParamsSchema +}); + +export const PaginatedRequestParamsSchema = BaseRequestParamsSchema.extend({ + /** + * An opaque token representing the current pagination position. + * If provided, the server should return results starting after this cursor. + */ + cursor: CursorSchema.optional() +}); + +/* Pagination */ +export const PaginatedRequestSchema = RequestSchema.extend({ + params: PaginatedRequestParamsSchema.optional() +}); + +export const PaginatedResultSchema = ResultSchema.extend({ + /** + * An opaque token representing the pagination position after the last returned result. + * If present, there may be more results available. + */ + nextCursor: CursorSchema.optional() +}); + +/** + * The status of a task. + * */ +export const TaskStatusSchema = z.enum(['working', 'input_required', 'completed', 'failed', 'cancelled']); + +/* Tasks */ +/** + * A pollable state object associated with a request. + */ +export const TaskSchema = z.object({ + taskId: z.string(), + status: TaskStatusSchema, + /** + * Time in milliseconds to keep task results available after completion. + * If `null`, the task has unlimited lifetime until manually cleaned up. + */ + ttl: z.union([z.number(), z.null()]), + /** + * ISO 8601 timestamp when the task was created. + */ + createdAt: z.string(), + /** + * ISO 8601 timestamp when the task was last updated. + */ + lastUpdatedAt: z.string(), + pollInterval: z.optional(z.number()), + /** + * Optional diagnostic message for failed tasks or other status information. + */ + statusMessage: z.optional(z.string()) +}); + +/** + * Result returned when a task is created, containing the task data wrapped in a `task` field. + */ +export const CreateTaskResultSchema = ResultSchema.extend({ + task: TaskSchema +}); + +/** + * Parameters for task status notification. + */ +export const TaskStatusNotificationParamsSchema = NotificationsParamsSchema.merge(TaskSchema); + +/** + * A notification sent when a task's status changes. + */ +export const TaskStatusNotificationSchema = NotificationSchema.extend({ + method: z.literal('notifications/tasks/status'), + params: TaskStatusNotificationParamsSchema +}); + +/** + * A request to get the state of a specific task. + */ +export const GetTaskRequestSchema = RequestSchema.extend({ + method: z.literal('tasks/get'), + params: BaseRequestParamsSchema.extend({ + taskId: z.string() + }) +}); + +/** + * The response to a {@linkcode GetTaskRequest | tasks/get} request. + */ +export const GetTaskResultSchema = ResultSchema.merge(TaskSchema); + +/** + * A request to get the result of a specific task. + */ +export const GetTaskPayloadRequestSchema = RequestSchema.extend({ + method: z.literal('tasks/result'), + params: BaseRequestParamsSchema.extend({ + taskId: z.string() + }) +}); + +/** + * The response to a `tasks/result` request. + * The structure matches the result type of the original request. + * For example, a {@linkcode CallToolRequest | tools/call} task would return the `CallToolResult` structure. + * + */ +export const GetTaskPayloadResultSchema = ResultSchema.loose(); + +/** + * A request to list tasks. + */ +export const ListTasksRequestSchema = PaginatedRequestSchema.extend({ + method: z.literal('tasks/list') +}); + +/** + * The response to a {@linkcode ListTasksRequest | tasks/list} request. + */ +export const ListTasksResultSchema = PaginatedResultSchema.extend({ + tasks: z.array(TaskSchema) +}); + +/** + * A request to cancel a specific task. + */ +export const CancelTaskRequestSchema = RequestSchema.extend({ + method: z.literal('tasks/cancel'), + params: BaseRequestParamsSchema.extend({ + taskId: z.string() + }) +}); + +/** + * The response to a {@linkcode CancelTaskRequest | tasks/cancel} request. + */ +export const CancelTaskResultSchema = ResultSchema.merge(TaskSchema); + +/* Resources */ +/** + * The contents of a specific resource or sub-resource. + */ +export const ResourceContentsSchema = z.object({ + /** + * The URI of this resource. + */ + uri: z.string(), + /** + * The MIME type of this resource, if known. + */ + mimeType: z.optional(z.string()), + /** + * See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields) + * for notes on `_meta` usage. + */ + _meta: z.record(z.string(), z.unknown()).optional() +}); + +export const TextResourceContentsSchema = ResourceContentsSchema.extend({ + /** + * The text of the item. This must only be set if the item can actually be represented as text (not binary data). + */ + text: z.string() +}); + +/** + * A Zod schema for validating Base64 strings that is more performant and + * robust for very large inputs than the default regex-based check. It avoids + * stack overflows by using the native `atob` function for validation. + */ +const Base64Schema = z.string().refine( + val => { + try { + // atob throws a DOMException if the string contains characters + // that are not part of the Base64 character set. + atob(val); + return true; + } catch { + return false; + } + }, + { message: 'Invalid Base64 string' } +); + +export const BlobResourceContentsSchema = ResourceContentsSchema.extend({ + /** + * A base64-encoded string representing the binary data of the item. + */ + blob: Base64Schema +}); + +/** + * The sender or recipient of messages and data in a conversation. + */ +export const RoleSchema = z.enum(['user', 'assistant']); + +/** + * Optional annotations providing clients additional context about a resource. + */ +export const AnnotationsSchema = z.object({ + /** + * Intended audience(s) for the resource. + */ + audience: z.array(RoleSchema).optional(), + + /** + * Importance hint for the resource, from 0 (least) to 1 (most). + */ + priority: z.number().min(0).max(1).optional(), + + /** + * ISO 8601 timestamp for the most recent modification. + */ + lastModified: z.iso.datetime({ offset: true }).optional() +}); + +/** + * A known resource that the server is capable of reading. + */ +export const ResourceSchema = z.object({ + ...BaseMetadataSchema.shape, + ...IconsSchema.shape, + /** + * The URI of this resource. + */ + uri: z.string(), + + /** + * A description of what this resource represents. + * + * This can be used by clients to improve the LLM's understanding of available resources. It can be thought of like a "hint" to the model. + */ + description: z.optional(z.string()), + + /** + * The MIME type of this resource, if known. + */ + mimeType: z.optional(z.string()), + + /** + * The size of the raw resource content, in bytes (i.e., before base64 encoding or any tokenization), if known. + * + * This can be used by Hosts to display file sizes and estimate context window usage. + */ + size: z.optional(z.number()), + + /** + * Optional annotations for the client. + */ + annotations: AnnotationsSchema.optional(), + + /** + * See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields) + * for notes on `_meta` usage. + */ + _meta: z.optional(z.looseObject({})) +}); + +/** + * A template description for resources available on the server. + */ +export const ResourceTemplateSchema = z.object({ + ...BaseMetadataSchema.shape, + ...IconsSchema.shape, + /** + * A URI template (according to RFC 6570) that can be used to construct resource URIs. + */ + uriTemplate: z.string(), + + /** + * A description of what this template is for. + * + * This can be used by clients to improve the LLM's understanding of available resources. It can be thought of like a "hint" to the model. + */ + description: z.optional(z.string()), + + /** + * The MIME type for all resources that match this template. This should only be included if all resources matching this template have the same type. + */ + mimeType: z.optional(z.string()), + + /** + * Optional annotations for the client. + */ + annotations: AnnotationsSchema.optional(), + + /** + * See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields) + * for notes on `_meta` usage. + */ + _meta: z.optional(z.looseObject({})) +}); + +/** + * Sent from the client to request a list of resources the server has. + */ +export const ListResourcesRequestSchema = PaginatedRequestSchema.extend({ + method: z.literal('resources/list') +}); + +/** + * The server's response to a {@linkcode ListResourcesRequest | resources/list} request from the client. + */ +export const ListResourcesResultSchema = PaginatedResultSchema.extend({ + resources: z.array(ResourceSchema) +}); + +/** + * Sent from the client to request a list of resource templates the server has. + */ +export const ListResourceTemplatesRequestSchema = PaginatedRequestSchema.extend({ + method: z.literal('resources/templates/list') +}); + +/** + * The server's response to a {@linkcode ListResourceTemplatesRequest | resources/templates/list} request from the client. + */ +export const ListResourceTemplatesResultSchema = PaginatedResultSchema.extend({ + resourceTemplates: z.array(ResourceTemplateSchema) +}); + +export const ResourceRequestParamsSchema = BaseRequestParamsSchema.extend({ + /** + * The URI of the resource to read. The URI can use any protocol; it is up to the server how to interpret it. + * + * @format uri + */ + uri: z.string() +}); + +/** + * Parameters for a {@linkcode ReadResourceRequest | resources/read} request. + */ +export const ReadResourceRequestParamsSchema = ResourceRequestParamsSchema; + +/** + * Sent from the client to the server, to read a specific resource URI. + */ +export const ReadResourceRequestSchema = RequestSchema.extend({ + method: z.literal('resources/read'), + params: ReadResourceRequestParamsSchema +}); + +/** + * The server's response to a {@linkcode ReadResourceRequest | resources/read} request from the client. + */ +export const ReadResourceResultSchema = ResultSchema.extend({ + contents: z.array(z.union([TextResourceContentsSchema, BlobResourceContentsSchema])) +}); + +/** + * An optional notification from the server to the client, informing it that the list of resources it can read from has changed. This may be issued by servers without any previous subscription from the client. + */ +export const ResourceListChangedNotificationSchema = NotificationSchema.extend({ + method: z.literal('notifications/resources/list_changed'), + params: NotificationsParamsSchema.optional() +}); + +export const SubscribeRequestParamsSchema = ResourceRequestParamsSchema; +/** + * Sent from the client to request `resources/updated` notifications from the server whenever a particular resource changes. + */ +export const SubscribeRequestSchema = RequestSchema.extend({ + method: z.literal('resources/subscribe'), + params: SubscribeRequestParamsSchema +}); + +export const UnsubscribeRequestParamsSchema = ResourceRequestParamsSchema; +/** + * Sent from the client to request cancellation of {@linkcode ResourceUpdatedNotification | resources/updated} notifications from the server. This should follow a previous {@linkcode SubscribeRequest | resources/subscribe} request. + */ +export const UnsubscribeRequestSchema = RequestSchema.extend({ + method: z.literal('resources/unsubscribe'), + params: UnsubscribeRequestParamsSchema +}); + +/** + * Parameters for a {@linkcode ResourceUpdatedNotification | notifications/resources/updated} notification. + */ +export const ResourceUpdatedNotificationParamsSchema = NotificationsParamsSchema.extend({ + /** + * The URI of the resource that has been updated. This might be a sub-resource of the one that the client actually subscribed to. + */ + uri: z.string() +}); + +/** + * A notification from the server to the client, informing it that a resource has changed and may need to be read again. This should only be sent if the client previously sent a {@linkcode SubscribeRequest | resources/subscribe} request. + */ +export const ResourceUpdatedNotificationSchema = NotificationSchema.extend({ + method: z.literal('notifications/resources/updated'), + params: ResourceUpdatedNotificationParamsSchema +}); + +/* Prompts */ +/** + * Describes an argument that a prompt can accept. + */ +export const PromptArgumentSchema = z.object({ + /** + * The name of the argument. + */ + name: z.string(), + /** + * A human-readable description of the argument. + */ + description: z.optional(z.string()), + /** + * Whether this argument must be provided. + */ + required: z.optional(z.boolean()) +}); + +/** + * A prompt or prompt template that the server offers. + */ +export const PromptSchema = z.object({ + ...BaseMetadataSchema.shape, + ...IconsSchema.shape, + /** + * An optional description of what this prompt provides + */ + description: z.optional(z.string()), + /** + * A list of arguments to use for templating the prompt. + */ + arguments: z.optional(z.array(PromptArgumentSchema)), + /** + * See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields) + * for notes on `_meta` usage. + */ + _meta: z.optional(z.looseObject({})) +}); + +/** + * Sent from the client to request a list of prompts and prompt templates the server has. + */ +export const ListPromptsRequestSchema = PaginatedRequestSchema.extend({ + method: z.literal('prompts/list') +}); + +/** + * The server's response to a {@linkcode ListPromptsRequest | prompts/list} request from the client. + */ +export const ListPromptsResultSchema = PaginatedResultSchema.extend({ + prompts: z.array(PromptSchema) +}); + +/** + * Parameters for a {@linkcode GetPromptRequest | prompts/get} request. + */ +export const GetPromptRequestParamsSchema = BaseRequestParamsSchema.extend({ + /** + * The name of the prompt or prompt template. + */ + name: z.string(), + /** + * Arguments to use for templating the prompt. + */ + arguments: z.record(z.string(), z.string()).optional() +}); +/** + * Used by the client to get a prompt provided by the server. + */ +export const GetPromptRequestSchema = RequestSchema.extend({ + method: z.literal('prompts/get'), + params: GetPromptRequestParamsSchema +}); + +/** + * Text provided to or from an LLM. + */ +export const TextContentSchema = z.object({ + type: z.literal('text'), + /** + * The text content of the message. + */ + text: z.string(), + + /** + * Optional annotations for the client. + */ + annotations: AnnotationsSchema.optional(), + + /** + * See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields) + * for notes on `_meta` usage. + */ + _meta: z.record(z.string(), z.unknown()).optional() +}); + +/** + * An image provided to or from an LLM. + */ +export const ImageContentSchema = z.object({ + type: z.literal('image'), + /** + * The base64-encoded image data. + */ + data: Base64Schema, + /** + * The MIME type of the image. Different providers may support different image types. + */ + mimeType: z.string(), + + /** + * Optional annotations for the client. + */ + annotations: AnnotationsSchema.optional(), + + /** + * See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields) + * for notes on `_meta` usage. + */ + _meta: z.record(z.string(), z.unknown()).optional() +}); + +/** + * Audio content provided to or from an LLM. + */ +export const AudioContentSchema = z.object({ + type: z.literal('audio'), + /** + * The base64-encoded audio data. + */ + data: Base64Schema, + /** + * The MIME type of the audio. Different providers may support different audio types. + */ + mimeType: z.string(), + + /** + * Optional annotations for the client. + */ + annotations: AnnotationsSchema.optional(), + + /** + * See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields) + * for notes on `_meta` usage. + */ + _meta: z.record(z.string(), z.unknown()).optional() +}); + +/** + * A tool call request from an assistant (LLM). + * Represents the assistant's request to use a tool. + */ +export const ToolUseContentSchema = z.object({ + type: z.literal('tool_use'), + /** + * The name of the tool to invoke. + * Must match a tool name from the request's tools array. + */ + name: z.string(), + /** + * Unique identifier for this tool call. + * Used to correlate with `ToolResultContent` in subsequent messages. + */ + id: z.string(), + /** + * Arguments to pass to the tool. + * Must conform to the tool's `inputSchema`. + */ + input: z.record(z.string(), z.unknown()), + /** + * See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields) + * for notes on `_meta` usage. + */ + _meta: z.record(z.string(), z.unknown()).optional() +}); + +/** + * The contents of a resource, embedded into a prompt or tool call result. + */ +export const EmbeddedResourceSchema = z.object({ + type: z.literal('resource'), + resource: z.union([TextResourceContentsSchema, BlobResourceContentsSchema]), + /** + * Optional annotations for the client. + */ + annotations: AnnotationsSchema.optional(), + /** + * See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields) + * for notes on `_meta` usage. + */ + _meta: z.record(z.string(), z.unknown()).optional() +}); + +/** + * A resource that the server is capable of reading, included in a prompt or tool call result. + * + * Note: resource links returned by tools are not guaranteed to appear in the results of {@linkcode ListResourcesRequest | resources/list} requests. + */ +export const ResourceLinkSchema = ResourceSchema.extend({ + type: z.literal('resource_link') +}); + +/** + * A content block that can be used in prompts and tool results. + */ +export const ContentBlockSchema = z.union([ + TextContentSchema, + ImageContentSchema, + AudioContentSchema, + ResourceLinkSchema, + EmbeddedResourceSchema +]); + +/** + * Describes a message returned as part of a prompt. + */ +export const PromptMessageSchema = z.object({ + role: RoleSchema, + content: ContentBlockSchema +}); + +/** + * The server's response to a {@linkcode GetPromptRequest | prompts/get} request from the client. + */ +export const GetPromptResultSchema = ResultSchema.extend({ + /** + * An optional description for the prompt. + */ + description: z.string().optional(), + messages: z.array(PromptMessageSchema) +}); + +/** + * An optional notification from the server to the client, informing it that the list of prompts it offers has changed. This may be issued by servers without any previous subscription from the client. + */ +export const PromptListChangedNotificationSchema = NotificationSchema.extend({ + method: z.literal('notifications/prompts/list_changed'), + params: NotificationsParamsSchema.optional() +}); + +/* Tools */ +/** + * Additional properties describing a `Tool` to clients. + * + * NOTE: all properties in {@linkcode ToolAnnotations} are **hints**. + * They are not guaranteed to provide a faithful description of + * tool behavior (including descriptive properties like `title`). + * + * Clients should never make tool use decisions based on `ToolAnnotations` + * received from untrusted servers. + */ +export const ToolAnnotationsSchema = z.object({ + /** + * A human-readable title for the tool. + */ + title: z.string().optional(), + + /** + * If `true`, the tool does not modify its environment. + * + * Default: `false` + */ + readOnlyHint: z.boolean().optional(), + + /** + * If `true`, the tool may perform destructive updates to its environment. + * If `false`, the tool performs only additive updates. + * + * (This property is meaningful only when `readOnlyHint == false`) + * + * Default: `true` + */ + destructiveHint: z.boolean().optional(), + + /** + * If `true`, calling the tool repeatedly with the same arguments + * will have no additional effect on its environment. + * + * (This property is meaningful only when `readOnlyHint == false`) + * + * Default: `false` + */ + idempotentHint: z.boolean().optional(), + + /** + * If `true`, this tool may interact with an "open world" of external + * entities. If `false`, the tool's domain of interaction is closed. + * For example, the world of a web search tool is open, whereas that + * of a memory tool is not. + * + * Default: `true` + */ + openWorldHint: z.boolean().optional() +}); + +/** + * Execution-related properties for a tool. + */ +export const ToolExecutionSchema = z.object({ + /** + * Indicates the tool's preference for task-augmented execution. + * - `"required"`: Clients MUST invoke the tool as a task + * - `"optional"`: Clients MAY invoke the tool as a task or normal request + * - `"forbidden"`: Clients MUST NOT attempt to invoke the tool as a task + * + * If not present, defaults to `"forbidden"`. + */ + taskSupport: z.enum(['required', 'optional', 'forbidden']).optional() +}); + +/** + * Definition for a tool the client can call. + */ +export const ToolSchema = z.object({ + ...BaseMetadataSchema.shape, + ...IconsSchema.shape, + /** + * A human-readable description of the tool. + */ + description: z.string().optional(), + /** + * A JSON Schema 2020-12 object defining the expected parameters for the tool. + * Must have `type: 'object'` at the root level per MCP spec. + */ + inputSchema: z + .object({ + type: z.literal('object'), + properties: z.record(z.string(), JSONValueSchema).optional(), + required: z.array(z.string()).optional() + }) + .catchall(z.unknown()), + /** + * An optional JSON Schema 2020-12 object defining the structure of the tool's output + * returned in the `structuredContent` field of a `CallToolResult`. + * Must have `type: 'object'` at the root level per MCP spec. + */ + outputSchema: z + .object({ + type: z.literal('object'), + properties: z.record(z.string(), JSONValueSchema).optional(), + required: z.array(z.string()).optional() + }) + .catchall(z.unknown()) + .optional(), + /** + * Optional additional tool information. + */ + annotations: ToolAnnotationsSchema.optional(), + /** + * Execution-related properties for this tool. + */ + execution: ToolExecutionSchema.optional(), + + /** + * See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields) + * for notes on `_meta` usage. + */ + _meta: z.record(z.string(), z.unknown()).optional() +}); + +/** + * Sent from the client to request a list of tools the server has. + */ +export const ListToolsRequestSchema = PaginatedRequestSchema.extend({ + method: z.literal('tools/list') +}); + +/** + * The server's response to a {@linkcode ListToolsRequest | tools/list} request from the client. + */ +export const ListToolsResultSchema = PaginatedResultSchema.extend({ + tools: z.array(ToolSchema) +}); + +/** + * The server's response to a tool call. + */ +export const CallToolResultSchema = ResultSchema.extend({ + /** + * A list of content objects that represent the result of the tool call. + * + * If the `Tool` does not define an outputSchema, this field MUST be present in the result. + * For backwards compatibility, this field is always present, but it may be empty. + */ + content: z.array(ContentBlockSchema).default([]), + + /** + * An object containing structured tool output. + * + * If the `Tool` defines an outputSchema, this field MUST be present in the result, and contain a JSON object that matches the schema. + */ + structuredContent: z.record(z.string(), z.unknown()).optional(), + + /** + * Whether the tool call ended in an error. + * + * If not set, this is assumed to be `false` (the call was successful). + * + * Any errors that originate from the tool SHOULD be reported inside the result + * object, with `isError` set to `true`, _not_ as an MCP protocol-level error + * response. Otherwise, the LLM would not be able to see that an error occurred + * and self-correct. + * + * However, any errors in _finding_ the tool, an error indicating that the + * server does not support tool calls, or any other exceptional conditions, + * should be reported as an MCP error response. + */ + isError: z.boolean().optional() +}); + +/** + * {@linkcode CallToolResultSchema} extended with backwards compatibility to protocol version 2024-10-07. + */ +export const CompatibilityCallToolResultSchema = CallToolResultSchema.or( + ResultSchema.extend({ + toolResult: z.unknown() + }) +); + +/** + * Parameters for a `tools/call` request. + */ +export const CallToolRequestParamsSchema = TaskAugmentedRequestParamsSchema.extend({ + /** + * The name of the tool to call. + */ + name: z.string(), + /** + * Arguments to pass to the tool. + */ + arguments: z.record(z.string(), z.unknown()).optional() +}); + +/** + * Used by the client to invoke a tool provided by the server. + */ +export const CallToolRequestSchema = RequestSchema.extend({ + method: z.literal('tools/call'), + params: CallToolRequestParamsSchema +}); + +/** + * An optional notification from the server to the client, informing it that the list of tools it offers has changed. This may be issued by servers without any previous subscription from the client. + */ +export const ToolListChangedNotificationSchema = NotificationSchema.extend({ + method: z.literal('notifications/tools/list_changed'), + params: NotificationsParamsSchema.optional() +}); + +/** + * Base schema for list changed subscription options (without callback). + * Used internally for Zod validation of `autoRefresh` and `debounceMs`. + */ +export const ListChangedOptionsBaseSchema = z.object({ + /** + * If `true`, the list will be refreshed automatically when a list changed notification is received. + * The callback will be called with the updated list. + * + * If `false`, the callback will be called with `null` items, allowing manual refresh. + * + * @default true + */ + autoRefresh: z.boolean().default(true), + /** + * Debounce time in milliseconds for list changed notification processing. + * + * Multiple notifications received within this timeframe will only trigger one refresh. + * Set to `0` to disable debouncing. + * + * @default 300 + */ + debounceMs: z.number().int().nonnegative().default(300) +}); + +/* Logging */ +/** + * The severity of a log message. + */ +export const LoggingLevelSchema = z.enum(['debug', 'info', 'notice', 'warning', 'error', 'critical', 'alert', 'emergency']); + +/** + * Parameters for a `logging/setLevel` request. + */ +export const SetLevelRequestParamsSchema = BaseRequestParamsSchema.extend({ + /** + * The level of logging that the client wants to receive from the server. The server should send all logs at this level and higher (i.e., more severe) to the client as `notifications/logging/message`. + */ + level: LoggingLevelSchema +}); +/** + * A request from the client to the server, to enable or adjust logging. + */ +export const SetLevelRequestSchema = RequestSchema.extend({ + method: z.literal('logging/setLevel'), + params: SetLevelRequestParamsSchema +}); + +/** + * Parameters for a `notifications/message` notification. + */ +export const LoggingMessageNotificationParamsSchema = NotificationsParamsSchema.extend({ + /** + * The severity of this log message. + */ + level: LoggingLevelSchema, + /** + * An optional name of the logger issuing this message. + */ + logger: z.string().optional(), + /** + * The data to be logged, such as a string message or an object. Any JSON serializable type is allowed here. + */ + data: z.unknown() +}); +/** + * Notification of a log message passed from server to client. If no `logging/setLevel` request has been sent from the client, the server MAY decide which messages to send automatically. + */ +export const LoggingMessageNotificationSchema = NotificationSchema.extend({ + method: z.literal('notifications/message'), + params: LoggingMessageNotificationParamsSchema +}); + +/* Per-request `_meta` envelope */ +/** + * The per-request `_meta` envelope carried by every request under protocol revision + * 2026-07-28: the protocol version governing the request, the client implementation + * info, and the client's capabilities — declared per request rather than once at + * initialization — plus the optional log-level opt-in. + * + * This schema models the complete envelope on its own. The base request schemas + * ({@linkcode RequestMetaSchema}) deliberately stay lenient so the same wire schemas + * parse requests from earlier protocol revisions (no envelope) as well; envelope + * requiredness is enforced per request at dispatch time, not here. + */ +export const RequestMetaEnvelopeSchema = z.looseObject({ + /** + * If specified, the caller is requesting out-of-band progress notifications for this request (as represented by notifications/progress). The value of this parameter is an opaque token that will be attached to any subsequent notifications. The receiver is not obligated to provide these notifications. + */ + progressToken: ProgressTokenSchema.optional(), + /** + * The MCP protocol version being used for this request. For the HTTP transport, + * the value must match the `MCP-Protocol-Version` header. + */ + [PROTOCOL_VERSION_META_KEY]: z.string(), + /** + * Identifies the client software making the request. + */ + [CLIENT_INFO_META_KEY]: ImplementationSchema, + /** + * The client's capabilities for this specific request. An empty object means the + * client supports no optional capabilities. Servers must not infer capabilities + * from prior requests. + */ + [CLIENT_CAPABILITIES_META_KEY]: ClientCapabilitiesSchema, + /** + * The desired log level for this request. When absent, the server must not send + * `notifications/message` notifications for the request. + * + * @deprecated Deprecated as of protocol version 2026-07-28 (SEP-2577); remains + * in the specification for at least twelve months. + */ + [LOG_LEVEL_META_KEY]: LoggingLevelSchema.optional() +}); + +/* Sampling */ +/** + * Hints to use for model selection. + */ +export const ModelHintSchema = z.object({ + /** + * A hint for a model name. + */ + name: z.string().optional() +}); + +/** + * The server's preferences for model selection, requested of the client during sampling. + */ +export const ModelPreferencesSchema = z.object({ + /** + * Optional hints to use for model selection. + */ + hints: z.array(ModelHintSchema).optional(), + /** + * How much to prioritize cost when selecting a model. + */ + costPriority: z.number().min(0).max(1).optional(), + /** + * How much to prioritize sampling speed (latency) when selecting a model. + */ + speedPriority: z.number().min(0).max(1).optional(), + /** + * How much to prioritize intelligence and capabilities when selecting a model. + */ + intelligencePriority: z.number().min(0).max(1).optional() +}); + +/** + * Controls tool usage behavior in sampling requests. + */ +export const ToolChoiceSchema = z.object({ + /** + * Controls when tools are used: + * - `"auto"`: Model decides whether to use tools (default) + * - `"required"`: Model MUST use at least one tool before completing + * - `"none"`: Model MUST NOT use any tools + */ + mode: z.enum(['auto', 'required', 'none']).optional() +}); + +/** + * The result of a tool execution, provided by the user (server). + * Represents the outcome of invoking a tool requested via `ToolUseContent`. + */ +export const ToolResultContentSchema = z.object({ + type: z.literal('tool_result'), + toolUseId: z.string().describe('The unique identifier for the corresponding tool call.'), + content: z.array(ContentBlockSchema).default([]), + structuredContent: z.object({}).loose().optional(), + isError: z.boolean().optional(), + + /** + * See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields) + * for notes on `_meta` usage. + */ + _meta: z.record(z.string(), z.unknown()).optional() +}); + +/** + * Basic content types for sampling responses (without tool use). + * Used for backwards-compatible {@linkcode CreateMessageResult} when tools are not used. + */ +export const SamplingContentSchema = z.discriminatedUnion('type', [TextContentSchema, ImageContentSchema, AudioContentSchema]); + +/** + * Content block types allowed in sampling messages. + * This includes text, image, audio, tool use requests, and tool results. + */ +export const SamplingMessageContentBlockSchema = z.discriminatedUnion('type', [ + TextContentSchema, + ImageContentSchema, + AudioContentSchema, + ToolUseContentSchema, + ToolResultContentSchema +]); + +/** + * Describes a message issued to or received from an LLM API. + */ +export const SamplingMessageSchema = z.object({ + role: RoleSchema, + content: z.union([SamplingMessageContentBlockSchema, z.array(SamplingMessageContentBlockSchema)]), + /** + * See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields) + * for notes on `_meta` usage. + */ + _meta: z.record(z.string(), z.unknown()).optional() +}); + +/** + * Parameters for a `sampling/createMessage` request. + */ +export const CreateMessageRequestParamsSchema = TaskAugmentedRequestParamsSchema.extend({ + messages: z.array(SamplingMessageSchema), + /** + * The server's preferences for which model to select. The client MAY modify or omit this request. + */ + modelPreferences: ModelPreferencesSchema.optional(), + /** + * An optional system prompt the server wants to use for sampling. The client MAY modify or omit this prompt. + */ + systemPrompt: z.string().optional(), + /** + * A request to include context from one or more MCP servers (including the caller), to be attached to the prompt. + * The client MAY ignore this request. + * + * Default is `"none"`. Values `"thisServer"` and `"allServers"` are soft-deprecated. Servers SHOULD only use these values if the client + * declares `ClientCapabilities`.`sampling.context`. These values may be removed in future spec releases. + */ + includeContext: z.enum(['none', 'thisServer', 'allServers']).optional(), + temperature: z.number().optional(), + /** + * The requested maximum number of tokens to sample (to prevent runaway completions). + * + * The client MAY choose to sample fewer tokens than the requested maximum. + */ + maxTokens: z.number().int(), + stopSequences: z.array(z.string()).optional(), + /** + * Optional metadata to pass through to the LLM provider. The format of this metadata is provider-specific. + */ + metadata: JSONObjectSchema.optional(), + /** + * Tools that the model may use during generation. + * The client MUST return an error if this field is provided but `ClientCapabilities`.`sampling.tools` is not declared. + */ + tools: z.array(ToolSchema).optional(), + /** + * Controls how the model uses tools. + * The client MUST return an error if this field is provided but `ClientCapabilities`.`sampling.tools` is not declared. + * Default is `{ mode: "auto" }`. + */ + toolChoice: ToolChoiceSchema.optional() +}); +/** + * A request from the server to sample an LLM via the client. The client has full discretion over which model to select. The client should also inform the user before beginning sampling, to allow them to inspect the request (human in the loop) and decide whether to approve it. + */ +export const CreateMessageRequestSchema = RequestSchema.extend({ + method: z.literal('sampling/createMessage'), + params: CreateMessageRequestParamsSchema +}); + +/** + * The client's response to a `sampling/create_message` request from the server. + * This is the backwards-compatible version that returns single content (no arrays). + * Used when the request does not include tools. + */ +export const CreateMessageResultSchema = ResultSchema.extend({ + /** + * The name of the model that generated the message. + */ + model: z.string(), + /** + * The reason why sampling stopped, if known. + * + * Standard values: + * - `"endTurn"`: Natural end of the assistant's turn + * - `"stopSequence"`: A stop sequence was encountered + * - `"maxTokens"`: Maximum token limit was reached + * + * This field is an open string to allow for provider-specific stop reasons. + */ + stopReason: z.optional(z.enum(['endTurn', 'stopSequence', 'maxTokens']).or(z.string())), + role: RoleSchema, + /** + * Response content. Single content block (text, image, or audio). + */ + content: SamplingContentSchema +}); + +/** + * The client's response to a `sampling/create_message` request when tools were provided. + * This version supports array content for tool use flows. + */ +export const CreateMessageResultWithToolsSchema = ResultSchema.extend({ + /** + * The name of the model that generated the message. + */ + model: z.string(), + /** + * The reason why sampling stopped, if known. + * + * Standard values: + * - `"endTurn"`: Natural end of the assistant's turn + * - `"stopSequence"`: A stop sequence was encountered + * - `"maxTokens"`: Maximum token limit was reached + * - `"toolUse"`: The model wants to use one or more tools + * + * This field is an open string to allow for provider-specific stop reasons. + */ + stopReason: z.optional(z.enum(['endTurn', 'stopSequence', 'maxTokens', 'toolUse']).or(z.string())), + role: RoleSchema, + /** + * Response content. May be a single block or array. May include `ToolUseContent` if `stopReason` is `"toolUse"`. + */ + content: z.union([SamplingMessageContentBlockSchema, z.array(SamplingMessageContentBlockSchema)]) +}); + +/* Elicitation */ +/** + * Primitive schema definition for boolean fields. + */ +export const BooleanSchemaSchema = z.object({ + type: z.literal('boolean'), + title: z.string().optional(), + description: z.string().optional(), + default: z.boolean().optional() +}); + +/** + * Primitive schema definition for string fields. + */ +export const StringSchemaSchema = z.object({ + type: z.literal('string'), + title: z.string().optional(), + description: z.string().optional(), + minLength: z.number().optional(), + maxLength: z.number().optional(), + format: z.enum(['email', 'uri', 'date', 'date-time']).optional(), + default: z.string().optional() +}); + +/** + * Primitive schema definition for number fields. + */ +export const NumberSchemaSchema = z.object({ + type: z.enum(['number', 'integer']), + title: z.string().optional(), + description: z.string().optional(), + minimum: z.number().optional(), + maximum: z.number().optional(), + default: z.number().optional() +}); + +/** + * Schema for single-selection enumeration without display titles for options. + */ +export const UntitledSingleSelectEnumSchemaSchema = z.object({ + type: z.literal('string'), + title: z.string().optional(), + description: z.string().optional(), + enum: z.array(z.string()), + default: z.string().optional() +}); + +/** + * Schema for single-selection enumeration with display titles for each option. + */ +export const TitledSingleSelectEnumSchemaSchema = z.object({ + type: z.literal('string'), + title: z.string().optional(), + description: z.string().optional(), + oneOf: z.array( + z.object({ + const: z.string(), + title: z.string() + }) + ), + default: z.string().optional() +}); + +/** + * Use {@linkcode TitledSingleSelectEnumSchema} instead. + * This interface will be removed in a future version. + */ +export const LegacyTitledEnumSchemaSchema = z.object({ + type: z.literal('string'), + title: z.string().optional(), + description: z.string().optional(), + enum: z.array(z.string()), + enumNames: z.array(z.string()).optional(), + default: z.string().optional() +}); + +// Combined single selection enumeration +export const SingleSelectEnumSchemaSchema = z.union([UntitledSingleSelectEnumSchemaSchema, TitledSingleSelectEnumSchemaSchema]); + +/** + * Schema for multiple-selection enumeration without display titles for options. + */ +export const UntitledMultiSelectEnumSchemaSchema = z.object({ + type: z.literal('array'), + title: z.string().optional(), + description: z.string().optional(), + minItems: z.number().optional(), + maxItems: z.number().optional(), + items: z.object({ + type: z.literal('string'), + enum: z.array(z.string()) + }), + default: z.array(z.string()).optional() +}); + +/** + * Schema for multiple-selection enumeration with display titles for each option. + */ +export const TitledMultiSelectEnumSchemaSchema = z.object({ + type: z.literal('array'), + title: z.string().optional(), + description: z.string().optional(), + minItems: z.number().optional(), + maxItems: z.number().optional(), + items: z.object({ + anyOf: z.array( + z.object({ + const: z.string(), + title: z.string() + }) + ) + }), + default: z.array(z.string()).optional() +}); + +/** + * Combined schema for multiple-selection enumeration + */ +export const MultiSelectEnumSchemaSchema = z.union([UntitledMultiSelectEnumSchemaSchema, TitledMultiSelectEnumSchemaSchema]); + +/** + * Primitive schema definition for enum fields. + */ +export const EnumSchemaSchema = z.union([LegacyTitledEnumSchemaSchema, SingleSelectEnumSchemaSchema, MultiSelectEnumSchemaSchema]); + +/** + * Union of all primitive schema definitions. + */ +export const PrimitiveSchemaDefinitionSchema = z.union([EnumSchemaSchema, BooleanSchemaSchema, StringSchemaSchema, NumberSchemaSchema]); + +/** + * Parameters for an `elicitation/create` request for form-based elicitation. + */ +export const ElicitRequestFormParamsSchema = TaskAugmentedRequestParamsSchema.extend({ + /** + * The elicitation mode. + * + * Optional for backward compatibility. Clients MUST treat missing `mode` as `"form"`. + */ + mode: z.literal('form').optional(), + /** + * The message to present to the user describing what information is being requested. + */ + message: z.string(), + /** + * A restricted subset of JSON Schema. + * Only top-level properties are allowed, without nesting. + */ + requestedSchema: z + .object({ + type: z.literal('object'), + properties: z.record(z.string(), PrimitiveSchemaDefinitionSchema), + required: z.array(z.string()).optional() + }) + .catchall(z.unknown()) +}); + +/** + * Parameters for an {@linkcode ElicitRequest | elicitation/create} request for URL-based elicitation. + */ +export const ElicitRequestURLParamsSchema = TaskAugmentedRequestParamsSchema.extend({ + /** + * The elicitation mode. + */ + mode: z.literal('url'), + /** + * The message to present to the user explaining why the interaction is needed. + */ + message: z.string(), + /** + * The ID of the elicitation, which must be unique within the context of the server. + * The client MUST treat this ID as an opaque value. + */ + elicitationId: z.string(), + /** + * The URL that the user should navigate to. + */ + url: z.string().url() +}); + +/** + * The parameters for a request to elicit additional information from the user via the client. + */ +export const ElicitRequestParamsSchema = z.union([ElicitRequestFormParamsSchema, ElicitRequestURLParamsSchema]); + +/** + * A request from the server to elicit user input via the client. + * The client should present the message and form fields to the user (form mode) + * or navigate to a URL (URL mode). + */ +export const ElicitRequestSchema = RequestSchema.extend({ + method: z.literal('elicitation/create'), + params: ElicitRequestParamsSchema +}); + +/** + * Parameters for a {@linkcode ElicitationCompleteNotification | notifications/elicitation/complete} notification. + * + * @category notifications/elicitation/complete + */ +export const ElicitationCompleteNotificationParamsSchema = NotificationsParamsSchema.extend({ + /** + * The ID of the elicitation that completed. + */ + elicitationId: z.string() +}); + +/** + * A notification from the server to the client, informing it of a completion of an out-of-band elicitation request. + * + * @category notifications/elicitation/complete + */ +export const ElicitationCompleteNotificationSchema = NotificationSchema.extend({ + method: z.literal('notifications/elicitation/complete'), + params: ElicitationCompleteNotificationParamsSchema +}); + +/** + * The client's response to an {@linkcode ElicitRequest | elicitation/create} request from the server. + */ +export const ElicitResultSchema = ResultSchema.extend({ + /** + * The user action in response to the elicitation. + * - `"accept"`: User submitted the form/confirmed the action + * - `"decline"`: User explicitly declined the action + * - `"cancel"`: User dismissed without making an explicit choice + */ + action: z.enum(['accept', 'decline', 'cancel']), + /** + * The submitted form data, only present when action is `"accept"`. + * Contains values matching the requested schema. + * Per MCP spec, content is "typically omitted" for decline/cancel actions. + * We normalize `null` to `undefined` for leniency while maintaining type compatibility. + */ + content: z.preprocess( + val => (val === null ? undefined : val), + z.record(z.string(), z.union([z.string(), z.number(), z.boolean(), z.array(z.string())])).optional() + ) +}); + +/* Autocomplete */ +/** + * A reference to a resource or resource template definition. + */ +export const ResourceTemplateReferenceSchema = z.object({ + type: z.literal('ref/resource'), + /** + * The URI or URI template of the resource. + */ + uri: z.string() +}); + +/** + * Identifies a prompt. + */ +export const PromptReferenceSchema = z.object({ + type: z.literal('ref/prompt'), + /** + * The name of the prompt or prompt template + */ + name: z.string() +}); + +/** + * Parameters for a {@linkcode CompleteRequest | completion/complete} request. + */ +export const CompleteRequestParamsSchema = BaseRequestParamsSchema.extend({ + ref: z.union([PromptReferenceSchema, ResourceTemplateReferenceSchema]), + /** + * The argument's information + */ + argument: z.object({ + /** + * The name of the argument + */ + name: z.string(), + /** + * The value of the argument to use for completion matching. + */ + value: z.string() + }), + context: z + .object({ + /** + * Previously-resolved variables in a URI template or prompt. + */ + arguments: z.record(z.string(), z.string()).optional() + }) + .optional() +}); +/** + * A request from the client to the server, to ask for completion options. + */ +export const CompleteRequestSchema = RequestSchema.extend({ + method: z.literal('completion/complete'), + params: CompleteRequestParamsSchema +}); + +/** + * The server's response to a {@linkcode CompleteRequest | completion/complete} request + */ +export const CompleteResultSchema = ResultSchema.extend({ + completion: z.looseObject({ + /** + * An array of completion values. Must not exceed 100 items. + */ + values: z.array(z.string()).max(100), + /** + * The total number of completion options available. This can exceed the number of values actually sent in the response. + */ + total: z.optional(z.number().int()), + /** + * Indicates whether there are additional completion options beyond those provided in the current response, even if the exact total is unknown. + */ + hasMore: z.optional(z.boolean()) + }) +}); + +/* Roots */ +/** + * Represents a root directory or file that the server can operate on. + */ +export const RootSchema = z.object({ + /** + * The URI identifying the root. This *must* start with `file://` for now. + */ + uri: z.string().startsWith('file://'), + /** + * An optional name for the root. + */ + name: z.string().optional(), + + /** + * See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields) + * for notes on `_meta` usage. + */ + _meta: z.record(z.string(), z.unknown()).optional() +}); + +/** + * Sent from the server to request a list of root URIs from the client. + */ +export const ListRootsRequestSchema = RequestSchema.extend({ + method: z.literal('roots/list'), + params: BaseRequestParamsSchema.optional() +}); + +/** + * The client's response to a `roots/list` request from the server. + */ +export const ListRootsResultSchema = ResultSchema.extend({ + roots: z.array(RootSchema) +}); + +/** + * A notification from the client to the server, informing it that the list of roots has changed. + */ +export const RootsListChangedNotificationSchema = NotificationSchema.extend({ + method: z.literal('notifications/roots/list_changed'), + params: NotificationsParamsSchema.optional() +}); + +/* Client messages */ +export const ClientRequestSchema = z.union([ + PingRequestSchema, + InitializeRequestSchema, + CompleteRequestSchema, + SetLevelRequestSchema, + GetPromptRequestSchema, + ListPromptsRequestSchema, + ListResourcesRequestSchema, + ListResourceTemplatesRequestSchema, + ReadResourceRequestSchema, + SubscribeRequestSchema, + UnsubscribeRequestSchema, + CallToolRequestSchema, + ListToolsRequestSchema, + GetTaskRequestSchema, + GetTaskPayloadRequestSchema, + ListTasksRequestSchema, + CancelTaskRequestSchema +]); + +export const ClientNotificationSchema = z.union([ + CancelledNotificationSchema, + ProgressNotificationSchema, + InitializedNotificationSchema, + RootsListChangedNotificationSchema, + TaskStatusNotificationSchema +]); + +export const ClientResultSchema = z.union([ + EmptyResultSchema, + CreateMessageResultSchema, + CreateMessageResultWithToolsSchema, + ElicitResultSchema, + ListRootsResultSchema, + GetTaskResultSchema, + ListTasksResultSchema, + CreateTaskResultSchema +]); + +/* Server messages */ +export const ServerRequestSchema = z.union([ + PingRequestSchema, + CreateMessageRequestSchema, + ElicitRequestSchema, + ListRootsRequestSchema, + GetTaskRequestSchema, + GetTaskPayloadRequestSchema, + ListTasksRequestSchema, + CancelTaskRequestSchema +]); + +export const ServerNotificationSchema = z.union([ + CancelledNotificationSchema, + ProgressNotificationSchema, + LoggingMessageNotificationSchema, + ResourceUpdatedNotificationSchema, + ResourceListChangedNotificationSchema, + ToolListChangedNotificationSchema, + PromptListChangedNotificationSchema, + TaskStatusNotificationSchema, + ElicitationCompleteNotificationSchema +]); + +export const ServerResultSchema = z.union([ + EmptyResultSchema, + InitializeResultSchema, + CompleteResultSchema, + GetPromptResultSchema, + ListPromptsResultSchema, + ListResourcesResultSchema, + ListResourceTemplatesResultSchema, + ReadResourceResultSchema, + CallToolResultSchema, + ListToolsResultSchema, + GetTaskResultSchema, + ListTasksResultSchema, + CreateTaskResultSchema +]); + +/* Runtime schema lookup — result schemas by method */ +const resultSchemas: Record = { + ping: EmptyResultSchema, + initialize: InitializeResultSchema, + 'completion/complete': CompleteResultSchema, + 'logging/setLevel': EmptyResultSchema, + 'prompts/get': GetPromptResultSchema, + 'prompts/list': ListPromptsResultSchema, + 'resources/list': ListResourcesResultSchema, + 'resources/templates/list': ListResourceTemplatesResultSchema, + 'resources/read': ReadResourceResultSchema, + 'resources/subscribe': EmptyResultSchema, + 'resources/unsubscribe': EmptyResultSchema, + 'tools/call': z.union([CallToolResultSchema, CreateTaskResultSchema]), + 'tools/list': ListToolsResultSchema, + 'sampling/createMessage': z.union([CreateMessageResultWithToolsSchema, CreateTaskResultSchema]), + 'elicitation/create': z.union([ElicitResultSchema, CreateTaskResultSchema]), + 'roots/list': ListRootsResultSchema, + 'tasks/get': GetTaskResultSchema, + 'tasks/result': ResultSchema, + 'tasks/list': ListTasksResultSchema, + 'tasks/cancel': CancelTaskResultSchema +}; + +/** + * Gets the Zod schema for validating results of a given request method. + * Returns `undefined` for non-spec methods. + * @see getRequestSchema for explanation of the internal type assertion. + */ +export function getResultSchema(method: M): z.ZodType; +export function getResultSchema(method: string): z.ZodType | undefined; +export function getResultSchema(method: string): z.ZodType | undefined { + return resultSchemas[method as RequestMethod] as unknown as z.ZodType | undefined; +} + +/* Runtime schema lookup — request schemas by method */ +type RequestSchemaType = (typeof ClientRequestSchema.options)[number] | (typeof ServerRequestSchema.options)[number]; +type NotificationSchemaType = (typeof ClientNotificationSchema.options)[number] | (typeof ServerNotificationSchema.options)[number]; + +function buildSchemaMap(schemas: readonly T[]): Record { + const map: Record = {}; + for (const schema of schemas) { + const method = schema.shape.method.value; + map[method] = schema; + } + return map; +} + +const requestSchemas = buildSchemaMap([...ClientRequestSchema.options, ...ServerRequestSchema.options] as const) as Record< + RequestMethod, + RequestSchemaType +>; +const notificationSchemas = buildSchemaMap([...ClientNotificationSchema.options, ...ServerNotificationSchema.options] as const) as Record< + NotificationMethod, + NotificationSchemaType +>; + +/** + * Gets the Zod schema for a given request method. + * Returns `undefined` for non-spec methods. + * The return type is a ZodType that parses to RequestTypeMap[M], allowing callers + * to use schema.parse() without needing additional type assertions. + * + * Note: The internal cast is necessary because TypeScript can't correlate the + * Record-based schema lookup with the MethodToTypeMap-based RequestTypeMap + * when M is a generic type parameter. Both compute to the same type at + * instantiation, but TypeScript can't prove this statically. + */ +export function getRequestSchema(method: M): z.ZodType; +export function getRequestSchema(method: string): z.ZodType | undefined; +export function getRequestSchema(method: string): z.ZodType | undefined { + return requestSchemas[method as RequestMethod] as unknown as z.ZodType | undefined; +} + +/** + * Gets the Zod schema for a given notification method. + * Returns `undefined` for non-spec methods. + * @see getRequestSchema for explanation of the internal type assertion. + */ +export function getNotificationSchema(method: M): z.ZodType; +export function getNotificationSchema(method: string): z.ZodType | undefined; +export function getNotificationSchema(method: string): z.ZodType | undefined { + return notificationSchemas[method as NotificationMethod] as unknown as z.ZodType | undefined; +} diff --git a/packages/core-internal/src/types/spec.types.2025-11-25.ts b/packages/core-internal/src/types/spec.types.2025-11-25.ts new file mode 100644 index 0000000000..225a53c2d7 --- /dev/null +++ b/packages/core-internal/src/types/spec.types.2025-11-25.ts @@ -0,0 +1,2559 @@ +/** + * This file is automatically generated from the Model Context Protocol specification. + * + * Source: https://github.com/modelcontextprotocol/modelcontextprotocol + * Pulled from: https://raw.githubusercontent.com/modelcontextprotocol/modelcontextprotocol/main/schema/2025-11-25/schema.ts + * Last updated from commit: 357adac47ab2654b64799f994e6db8d3df4ee19d + * + * DO NOT EDIT THIS FILE MANUALLY. Changes will be overwritten by automated updates. + * To update this file, run: pnpm run fetch:spec-types 2025-11-25 + */ /* JSON-RPC types */ + +/** + * Refers to any valid JSON-RPC object that can be decoded off the wire, or encoded to be sent. + * + * @category JSON-RPC + */ +export type JSONRPCMessage = JSONRPCRequest | JSONRPCNotification | JSONRPCResponse; + +/** @internal */ +export const LATEST_PROTOCOL_VERSION = '2025-11-25'; +/** @internal */ +export const JSONRPC_VERSION = '2.0'; + +/** + * A progress token, used to associate progress notifications with the original request. + * + * @category Common Types + */ +export type ProgressToken = string | number; + +/** + * An opaque token used to represent a cursor for pagination. + * + * @category Common Types + */ +export type Cursor = string; + +/** + * Common params for any task-augmented request. + * + * @internal + */ +export interface TaskAugmentedRequestParams extends RequestParams { + /** + * If specified, the caller is requesting task-augmented execution for this request. + * The request will return a CreateTaskResult immediately, and the actual result can be + * retrieved later via tasks/result. + * + * Task augmentation is subject to capability negotiation - receivers MUST declare support + * for task augmentation of specific request types in their capabilities. + */ + task?: TaskMetadata; +} +/** + * Common params for any request. + * + * @internal + */ +export interface RequestParams { + /** + * See [General fields: `_meta`](/specification/2025-11-25/basic/index#meta) for notes on `_meta` usage. + */ + _meta?: { + /** + * If specified, the caller is requesting out-of-band progress notifications for this request (as represented by notifications/progress). The value of this parameter is an opaque token that will be attached to any subsequent notifications. The receiver is not obligated to provide these notifications. + */ + progressToken?: ProgressToken; + [key: string]: unknown; + }; +} + +/** @internal */ +export interface Request { + method: string; + // Allow unofficial extensions of `Request.params` without impacting `RequestParams`. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + params?: { [key: string]: any }; +} + +/** @internal */ +export interface NotificationParams { + /** + * See [General fields: `_meta`](/specification/2025-11-25/basic/index#meta) for notes on `_meta` usage. + */ + _meta?: { [key: string]: unknown }; +} + +/** @internal */ +export interface Notification { + method: string; + // Allow unofficial extensions of `Notification.params` without impacting `NotificationParams`. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + params?: { [key: string]: any }; +} + +/** + * @category Common Types + */ +export interface Result { + /** + * See [General fields: `_meta`](/specification/2025-11-25/basic/index#meta) for notes on `_meta` usage. + */ + _meta?: { [key: string]: unknown }; + [key: string]: unknown; +} + +/** + * @category Common Types + */ +export interface Error { + /** + * The error type that occurred. + */ + code: number; + /** + * A short description of the error. The message SHOULD be limited to a concise single sentence. + */ + message: string; + /** + * Additional information about the error. The value of this member is defined by the sender (e.g. detailed error information, nested errors etc.). + */ + data?: unknown; +} + +/** + * A uniquely identifying ID for a request in JSON-RPC. + * + * @category Common Types + */ +export type RequestId = string | number; + +/** + * A request that expects a response. + * + * @category JSON-RPC + */ +export interface JSONRPCRequest extends Request { + jsonrpc: typeof JSONRPC_VERSION; + id: RequestId; +} + +/** + * A notification which does not expect a response. + * + * @category JSON-RPC + */ +export interface JSONRPCNotification extends Notification { + jsonrpc: typeof JSONRPC_VERSION; +} + +/** + * A successful (non-error) response to a request. + * + * @category JSON-RPC + */ +export interface JSONRPCResultResponse { + jsonrpc: typeof JSONRPC_VERSION; + id: RequestId; + result: Result; +} + +/** + * A response to a request that indicates an error occurred. + * + * @category JSON-RPC + */ +export interface JSONRPCErrorResponse { + jsonrpc: typeof JSONRPC_VERSION; + id?: RequestId; + error: Error; +} + +/** + * A response to a request, containing either the result or error. + * + * @category JSON-RPC + */ +export type JSONRPCResponse = JSONRPCResultResponse | JSONRPCErrorResponse; + +// Standard JSON-RPC error codes +export const PARSE_ERROR = -32700; +export const INVALID_REQUEST = -32600; +export const METHOD_NOT_FOUND = -32601; +export const INVALID_PARAMS = -32602; +export const INTERNAL_ERROR = -32603; + +// Implementation-specific JSON-RPC error codes [-32000, -32099] +/** @internal */ +export const URL_ELICITATION_REQUIRED = -32042; + +/** + * An error response that indicates that the server requires the client to provide additional information via an elicitation request. + * + * @internal + */ +export interface URLElicitationRequiredError extends Omit { + error: Error & { + code: typeof URL_ELICITATION_REQUIRED; + data: { + elicitations: ElicitRequestURLParams[]; + [key: string]: unknown; + }; + }; +} + +/* Empty result */ +/** + * A response that indicates success but carries no data. + * + * @category Common Types + */ +export type EmptyResult = Result; + +/* Cancellation */ +/** + * Parameters for a `notifications/cancelled` notification. + * + * @category `notifications/cancelled` + */ +export interface CancelledNotificationParams extends NotificationParams { + /** + * The ID of the request to cancel. + * + * This MUST correspond to the ID of a request previously issued in the same direction. + * This MUST be provided for cancelling non-task requests. + * This MUST NOT be used for cancelling tasks (use the `tasks/cancel` request instead). + */ + requestId?: RequestId; + + /** + * An optional string describing the reason for the cancellation. This MAY be logged or presented to the user. + */ + reason?: string; +} + +/** + * This notification can be sent by either side to indicate that it is cancelling a previously-issued request. + * + * The request SHOULD still be in-flight, but due to communication latency, it is always possible that this notification MAY arrive after the request has already finished. + * + * This notification indicates that the result will be unused, so any associated processing SHOULD cease. + * + * A client MUST NOT attempt to cancel its `initialize` request. + * + * For task cancellation, use the `tasks/cancel` request instead of this notification. + * + * @category `notifications/cancelled` + */ +export interface CancelledNotification extends JSONRPCNotification { + method: 'notifications/cancelled'; + params: CancelledNotificationParams; +} + +/* Initialization */ +/** + * Parameters for an `initialize` request. + * + * @category `initialize` + */ +export interface InitializeRequestParams extends RequestParams { + /** + * The latest version of the Model Context Protocol that the client supports. The client MAY decide to support older versions as well. + */ + protocolVersion: string; + capabilities: ClientCapabilities; + clientInfo: Implementation; +} + +/** + * This request is sent from the client to the server when it first connects, asking it to begin initialization. + * + * @category `initialize` + */ +export interface InitializeRequest extends JSONRPCRequest { + method: 'initialize'; + params: InitializeRequestParams; +} + +/** + * After receiving an initialize request from the client, the server sends this response. + * + * @category `initialize` + */ +export interface InitializeResult extends Result { + /** + * The version of the Model Context Protocol that the server wants to use. This may not match the version that the client requested. If the client cannot support this version, it MUST disconnect. + */ + protocolVersion: string; + capabilities: ServerCapabilities; + serverInfo: Implementation; + + /** + * Instructions describing how to use the server and its features. + * + * This can be used by clients to improve the LLM's understanding of available tools, resources, etc. It can be thought of like a "hint" to the model. For example, this information MAY be added to the system prompt. + */ + instructions?: string; +} + +/** + * This notification is sent from the client to the server after initialization has finished. + * + * @category `notifications/initialized` + */ +export interface InitializedNotification extends JSONRPCNotification { + method: 'notifications/initialized'; + params?: NotificationParams; +} + +/** + * Capabilities a client may support. Known capabilities are defined here, in this schema, but this is not a closed set: any client can define its own, additional capabilities. + * + * @category `initialize` + */ +export interface ClientCapabilities { + /** + * Experimental, non-standard capabilities that the client supports. + */ + experimental?: { [key: string]: object }; + /** + * Present if the client supports listing roots. + */ + roots?: { + /** + * Whether the client supports notifications for changes to the roots list. + */ + listChanged?: boolean; + }; + /** + * Present if the client supports sampling from an LLM. + */ + sampling?: { + /** + * Whether the client supports context inclusion via includeContext parameter. + * If not declared, servers SHOULD only use `includeContext: "none"` (or omit it). + */ + context?: object; + /** + * Whether the client supports tool use via tools and toolChoice parameters. + */ + tools?: object; + }; + /** + * Present if the client supports elicitation from the server. + */ + elicitation?: { form?: object; url?: object }; + + /** + * Present if the client supports task-augmented requests. + */ + tasks?: { + /** + * Whether this client supports tasks/list. + */ + list?: object; + /** + * Whether this client supports tasks/cancel. + */ + cancel?: object; + /** + * Specifies which request types can be augmented with tasks. + */ + requests?: { + /** + * Task support for sampling-related requests. + */ + sampling?: { + /** + * Whether the client supports task-augmented sampling/createMessage requests. + */ + createMessage?: object; + }; + /** + * Task support for elicitation-related requests. + */ + elicitation?: { + /** + * Whether the client supports task-augmented elicitation/create requests. + */ + create?: object; + }; + }; + }; +} + +/** + * Capabilities that a server may support. Known capabilities are defined here, in this schema, but this is not a closed set: any server can define its own, additional capabilities. + * + * @category `initialize` + */ +export interface ServerCapabilities { + /** + * Experimental, non-standard capabilities that the server supports. + */ + experimental?: { [key: string]: object }; + /** + * Present if the server supports sending log messages to the client. + */ + logging?: object; + /** + * Present if the server supports argument autocompletion suggestions. + */ + completions?: object; + /** + * Present if the server offers any prompt templates. + */ + prompts?: { + /** + * Whether this server supports notifications for changes to the prompt list. + */ + listChanged?: boolean; + }; + /** + * Present if the server offers any resources to read. + */ + resources?: { + /** + * Whether this server supports subscribing to resource updates. + */ + subscribe?: boolean; + /** + * Whether this server supports notifications for changes to the resource list. + */ + listChanged?: boolean; + }; + /** + * Present if the server offers any tools to call. + */ + tools?: { + /** + * Whether this server supports notifications for changes to the tool list. + */ + listChanged?: boolean; + }; + /** + * Present if the server supports task-augmented requests. + */ + tasks?: { + /** + * Whether this server supports tasks/list. + */ + list?: object; + /** + * Whether this server supports tasks/cancel. + */ + cancel?: object; + /** + * Specifies which request types can be augmented with tasks. + */ + requests?: { + /** + * Task support for tool-related requests. + */ + tools?: { + /** + * Whether the server supports task-augmented tools/call requests. + */ + call?: object; + }; + }; + }; +} + +/** + * An optionally-sized icon that can be displayed in a user interface. + * + * @category Common Types + */ +export interface Icon { + /** + * A standard URI pointing to an icon resource. May be an HTTP/HTTPS URL or a + * `data:` URI with Base64-encoded image data. + * + * Consumers SHOULD takes steps to ensure URLs serving icons are from the + * same domain as the client/server or a trusted domain. + * + * Consumers SHOULD take appropriate precautions when consuming SVGs as they can contain + * executable JavaScript. + * + * @format uri + */ + src: string; + + /** + * Optional MIME type override if the source MIME type is missing or generic. + * For example: `"image/png"`, `"image/jpeg"`, or `"image/svg+xml"`. + */ + mimeType?: string; + + /** + * Optional array of strings that specify sizes at which the icon can be used. + * Each string should be in WxH format (e.g., `"48x48"`, `"96x96"`) or `"any"` for scalable formats like SVG. + * + * If not provided, the client should assume that the icon can be used at any size. + */ + sizes?: string[]; + + /** + * Optional specifier for the theme this icon is designed for. `light` indicates + * the icon is designed to be used with a light background, and `dark` indicates + * the icon is designed to be used with a dark background. + * + * If not provided, the client should assume the icon can be used with any theme. + */ + theme?: 'light' | 'dark'; +} + +/** + * Base interface to add `icons` property. + * + * @internal + */ +export interface Icons { + /** + * Optional set of sized icons that the client can display in a user interface. + * + * Clients that support rendering icons MUST support at least the following MIME types: + * - `image/png` - PNG images (safe, universal compatibility) + * - `image/jpeg` (and `image/jpg`) - JPEG images (safe, universal compatibility) + * + * Clients that support rendering icons SHOULD also support: + * - `image/svg+xml` - SVG images (scalable but requires security precautions) + * - `image/webp` - WebP images (modern, efficient format) + */ + icons?: Icon[]; +} + +/** + * Base interface for metadata with name (identifier) and title (display name) properties. + * + * @internal + */ +export interface BaseMetadata { + /** + * Intended for programmatic or logical use, but used as a display name in past specs or fallback (if title isn't present). + */ + name: string; + + /** + * Intended for UI and end-user contexts — optimized to be human-readable and easily understood, + * even by those unfamiliar with domain-specific terminology. + * + * If not provided, the name should be used for display (except for Tool, + * where `annotations.title` should be given precedence over using `name`, + * if present). + */ + title?: string; +} + +/** + * Describes the MCP implementation. + * + * @category `initialize` + */ +export interface Implementation extends BaseMetadata, Icons { + version: string; + + /** + * An optional human-readable description of what this implementation does. + * + * This can be used by clients or servers to provide context about their purpose + * and capabilities. For example, a server might describe the types of resources + * or tools it provides, while a client might describe its intended use case. + */ + description?: string; + + /** + * An optional URL of the website for this implementation. + * + * @format uri + */ + websiteUrl?: string; +} + +/* Ping */ +/** + * A ping, issued by either the server or the client, to check that the other party is still alive. The receiver must promptly respond, or else may be disconnected. + * + * @category `ping` + */ +export interface PingRequest extends JSONRPCRequest { + method: 'ping'; + params?: RequestParams; +} + +/* Progress notifications */ + +/** + * Parameters for a `notifications/progress` notification. + * + * @category `notifications/progress` + */ +export interface ProgressNotificationParams extends NotificationParams { + /** + * The progress token which was given in the initial request, used to associate this notification with the request that is proceeding. + */ + progressToken: ProgressToken; + /** + * The progress thus far. This should increase every time progress is made, even if the total is unknown. + * + * @TJS-type number + */ + progress: number; + /** + * Total number of items to process (or total progress required), if known. + * + * @TJS-type number + */ + total?: number; + /** + * An optional message describing the current progress. + */ + message?: string; +} + +/** + * An out-of-band notification used to inform the receiver of a progress update for a long-running request. + * + * @category `notifications/progress` + */ +export interface ProgressNotification extends JSONRPCNotification { + method: 'notifications/progress'; + params: ProgressNotificationParams; +} + +/* Pagination */ +/** + * Common parameters for paginated requests. + * + * @internal + */ +export interface PaginatedRequestParams extends RequestParams { + /** + * An opaque token representing the current pagination position. + * If provided, the server should return results starting after this cursor. + */ + cursor?: Cursor; +} + +/** @internal */ +export interface PaginatedRequest extends JSONRPCRequest { + params?: PaginatedRequestParams; +} + +/** @internal */ +export interface PaginatedResult extends Result { + /** + * An opaque token representing the pagination position after the last returned result. + * If present, there may be more results available. + */ + nextCursor?: Cursor; +} + +/* Resources */ +/** + * Sent from the client to request a list of resources the server has. + * + * @category `resources/list` + */ +export interface ListResourcesRequest extends PaginatedRequest { + method: 'resources/list'; +} + +/** + * The server's response to a resources/list request from the client. + * + * @category `resources/list` + */ +export interface ListResourcesResult extends PaginatedResult { + resources: Resource[]; +} + +/** + * Sent from the client to request a list of resource templates the server has. + * + * @category `resources/templates/list` + */ +export interface ListResourceTemplatesRequest extends PaginatedRequest { + method: 'resources/templates/list'; +} + +/** + * The server's response to a resources/templates/list request from the client. + * + * @category `resources/templates/list` + */ +export interface ListResourceTemplatesResult extends PaginatedResult { + resourceTemplates: ResourceTemplate[]; +} + +/** + * Common parameters when working with resources. + * + * @internal + */ +export interface ResourceRequestParams extends RequestParams { + /** + * The URI of the resource. The URI can use any protocol; it is up to the server how to interpret it. + * + * @format uri + */ + uri: string; +} + +/** + * Parameters for a `resources/read` request. + * + * @category `resources/read` + */ +// eslint-disable-next-line @typescript-eslint/no-empty-object-type +export interface ReadResourceRequestParams extends ResourceRequestParams {} + +/** + * Sent from the client to the server, to read a specific resource URI. + * + * @category `resources/read` + */ +export interface ReadResourceRequest extends JSONRPCRequest { + method: 'resources/read'; + params: ReadResourceRequestParams; +} + +/** + * The server's response to a resources/read request from the client. + * + * @category `resources/read` + */ +export interface ReadResourceResult extends Result { + contents: (TextResourceContents | BlobResourceContents)[]; +} + +/** + * An optional notification from the server to the client, informing it that the list of resources it can read from has changed. This may be issued by servers without any previous subscription from the client. + * + * @category `notifications/resources/list_changed` + */ +export interface ResourceListChangedNotification extends JSONRPCNotification { + method: 'notifications/resources/list_changed'; + params?: NotificationParams; +} + +/** + * Parameters for a `resources/subscribe` request. + * + * @category `resources/subscribe` + */ +// eslint-disable-next-line @typescript-eslint/no-empty-object-type +export interface SubscribeRequestParams extends ResourceRequestParams {} + +/** + * Sent from the client to request resources/updated notifications from the server whenever a particular resource changes. + * + * @category `resources/subscribe` + */ +export interface SubscribeRequest extends JSONRPCRequest { + method: 'resources/subscribe'; + params: SubscribeRequestParams; +} + +/** + * Parameters for a `resources/unsubscribe` request. + * + * @category `resources/unsubscribe` + */ +// eslint-disable-next-line @typescript-eslint/no-empty-object-type +export interface UnsubscribeRequestParams extends ResourceRequestParams {} + +/** + * Sent from the client to request cancellation of resources/updated notifications from the server. This should follow a previous resources/subscribe request. + * + * @category `resources/unsubscribe` + */ +export interface UnsubscribeRequest extends JSONRPCRequest { + method: 'resources/unsubscribe'; + params: UnsubscribeRequestParams; +} + +/** + * Parameters for a `notifications/resources/updated` notification. + * + * @category `notifications/resources/updated` + */ +export interface ResourceUpdatedNotificationParams extends NotificationParams { + /** + * The URI of the resource that has been updated. This might be a sub-resource of the one that the client actually subscribed to. + * + * @format uri + */ + uri: string; +} + +/** + * A notification from the server to the client, informing it that a resource has changed and may need to be read again. This should only be sent if the client previously sent a resources/subscribe request. + * + * @category `notifications/resources/updated` + */ +export interface ResourceUpdatedNotification extends JSONRPCNotification { + method: 'notifications/resources/updated'; + params: ResourceUpdatedNotificationParams; +} + +/** + * A known resource that the server is capable of reading. + * + * @category `resources/list` + */ +export interface Resource extends BaseMetadata, Icons { + /** + * The URI of this resource. + * + * @format uri + */ + uri: string; + + /** + * A description of what this resource represents. + * + * This can be used by clients to improve the LLM's understanding of available resources. It can be thought of like a "hint" to the model. + */ + description?: string; + + /** + * The MIME type of this resource, if known. + */ + mimeType?: string; + + /** + * Optional annotations for the client. + */ + annotations?: Annotations; + + /** + * The size of the raw resource content, in bytes (i.e., before base64 encoding or any tokenization), if known. + * + * This can be used by Hosts to display file sizes and estimate context window usage. + */ + size?: number; + + /** + * See [General fields: `_meta`](/specification/2025-11-25/basic/index#meta) for notes on `_meta` usage. + */ + _meta?: { [key: string]: unknown }; +} + +/** + * A template description for resources available on the server. + * + * @category `resources/templates/list` + */ +export interface ResourceTemplate extends BaseMetadata, Icons { + /** + * A URI template (according to RFC 6570) that can be used to construct resource URIs. + * + * @format uri-template + */ + uriTemplate: string; + + /** + * A description of what this template is for. + * + * This can be used by clients to improve the LLM's understanding of available resources. It can be thought of like a "hint" to the model. + */ + description?: string; + + /** + * The MIME type for all resources that match this template. This should only be included if all resources matching this template have the same type. + */ + mimeType?: string; + + /** + * Optional annotations for the client. + */ + annotations?: Annotations; + + /** + * See [General fields: `_meta`](/specification/2025-11-25/basic/index#meta) for notes on `_meta` usage. + */ + _meta?: { [key: string]: unknown }; +} + +/** + * The contents of a specific resource or sub-resource. + * + * @internal + */ +export interface ResourceContents { + /** + * The URI of this resource. + * + * @format uri + */ + uri: string; + /** + * The MIME type of this resource, if known. + */ + mimeType?: string; + + /** + * See [General fields: `_meta`](/specification/2025-11-25/basic/index#meta) for notes on `_meta` usage. + */ + _meta?: { [key: string]: unknown }; +} + +/** + * @category Content + */ +export interface TextResourceContents extends ResourceContents { + /** + * The text of the item. This must only be set if the item can actually be represented as text (not binary data). + */ + text: string; +} + +/** + * @category Content + */ +export interface BlobResourceContents extends ResourceContents { + /** + * A base64-encoded string representing the binary data of the item. + * + * @format byte + */ + blob: string; +} + +/* Prompts */ +/** + * Sent from the client to request a list of prompts and prompt templates the server has. + * + * @category `prompts/list` + */ +export interface ListPromptsRequest extends PaginatedRequest { + method: 'prompts/list'; +} + +/** + * The server's response to a prompts/list request from the client. + * + * @category `prompts/list` + */ +export interface ListPromptsResult extends PaginatedResult { + prompts: Prompt[]; +} + +/** + * Parameters for a `prompts/get` request. + * + * @category `prompts/get` + */ +export interface GetPromptRequestParams extends RequestParams { + /** + * The name of the prompt or prompt template. + */ + name: string; + /** + * Arguments to use for templating the prompt. + */ + arguments?: { [key: string]: string }; +} + +/** + * Used by the client to get a prompt provided by the server. + * + * @category `prompts/get` + */ +export interface GetPromptRequest extends JSONRPCRequest { + method: 'prompts/get'; + params: GetPromptRequestParams; +} + +/** + * The server's response to a prompts/get request from the client. + * + * @category `prompts/get` + */ +export interface GetPromptResult extends Result { + /** + * An optional description for the prompt. + */ + description?: string; + messages: PromptMessage[]; +} + +/** + * A prompt or prompt template that the server offers. + * + * @category `prompts/list` + */ +export interface Prompt extends BaseMetadata, Icons { + /** + * An optional description of what this prompt provides + */ + description?: string; + + /** + * A list of arguments to use for templating the prompt. + */ + arguments?: PromptArgument[]; + + /** + * See [General fields: `_meta`](/specification/2025-11-25/basic/index#meta) for notes on `_meta` usage. + */ + _meta?: { [key: string]: unknown }; +} + +/** + * Describes an argument that a prompt can accept. + * + * @category `prompts/list` + */ +export interface PromptArgument extends BaseMetadata { + /** + * A human-readable description of the argument. + */ + description?: string; + /** + * Whether this argument must be provided. + */ + required?: boolean; +} + +/** + * The sender or recipient of messages and data in a conversation. + * + * @category Common Types + */ +export type Role = 'user' | 'assistant'; + +/** + * Describes a message returned as part of a prompt. + * + * This is similar to `SamplingMessage`, but also supports the embedding of + * resources from the MCP server. + * + * @category `prompts/get` + */ +export interface PromptMessage { + role: Role; + content: ContentBlock; +} + +/** + * A resource that the server is capable of reading, included in a prompt or tool call result. + * + * Note: resource links returned by tools are not guaranteed to appear in the results of `resources/list` requests. + * + * @category Content + */ +export interface ResourceLink extends Resource { + type: 'resource_link'; +} + +/** + * The contents of a resource, embedded into a prompt or tool call result. + * + * It is up to the client how best to render embedded resources for the benefit + * of the LLM and/or the user. + * + * @category Content + */ +export interface EmbeddedResource { + type: 'resource'; + resource: TextResourceContents | BlobResourceContents; + + /** + * Optional annotations for the client. + */ + annotations?: Annotations; + + /** + * See [General fields: `_meta`](/specification/2025-11-25/basic/index#meta) for notes on `_meta` usage. + */ + _meta?: { [key: string]: unknown }; +} +/** + * An optional notification from the server to the client, informing it that the list of prompts it offers has changed. This may be issued by servers without any previous subscription from the client. + * + * @category `notifications/prompts/list_changed` + */ +export interface PromptListChangedNotification extends JSONRPCNotification { + method: 'notifications/prompts/list_changed'; + params?: NotificationParams; +} + +/* Tools */ +/** + * Sent from the client to request a list of tools the server has. + * + * @category `tools/list` + */ +export interface ListToolsRequest extends PaginatedRequest { + method: 'tools/list'; +} + +/** + * The server's response to a tools/list request from the client. + * + * @category `tools/list` + */ +export interface ListToolsResult extends PaginatedResult { + tools: Tool[]; +} + +/** + * The server's response to a tool call. + * + * @category `tools/call` + */ +export interface CallToolResult extends Result { + /** + * A list of content objects that represent the unstructured result of the tool call. + */ + content: ContentBlock[]; + + /** + * An optional JSON object that represents the structured result of the tool call. + */ + structuredContent?: { [key: string]: unknown }; + + /** + * Whether the tool call ended in an error. + * + * If not set, this is assumed to be false (the call was successful). + * + * Any errors that originate from the tool SHOULD be reported inside the result + * object, with `isError` set to true, _not_ as an MCP protocol-level error + * response. Otherwise, the LLM would not be able to see that an error occurred + * and self-correct. + * + * However, any errors in _finding_ the tool, an error indicating that the + * server does not support tool calls, or any other exceptional conditions, + * should be reported as an MCP error response. + */ + isError?: boolean; +} + +/** + * Parameters for a `tools/call` request. + * + * @category `tools/call` + */ +export interface CallToolRequestParams extends TaskAugmentedRequestParams { + /** + * The name of the tool. + */ + name: string; + /** + * Arguments to use for the tool call. + */ + arguments?: { [key: string]: unknown }; +} + +/** + * Used by the client to invoke a tool provided by the server. + * + * @category `tools/call` + */ +export interface CallToolRequest extends JSONRPCRequest { + method: 'tools/call'; + params: CallToolRequestParams; +} + +/** + * An optional notification from the server to the client, informing it that the list of tools it offers has changed. This may be issued by servers without any previous subscription from the client. + * + * @category `notifications/tools/list_changed` + */ +export interface ToolListChangedNotification extends JSONRPCNotification { + method: 'notifications/tools/list_changed'; + params?: NotificationParams; +} + +/** + * Additional properties describing a Tool to clients. + * + * NOTE: all properties in ToolAnnotations are **hints**. + * They are not guaranteed to provide a faithful description of + * tool behavior (including descriptive properties like `title`). + * + * Clients should never make tool use decisions based on ToolAnnotations + * received from untrusted servers. + * + * @category `tools/list` + */ +export interface ToolAnnotations { + /** + * A human-readable title for the tool. + */ + title?: string; + + /** + * If true, the tool does not modify its environment. + * + * Default: false + */ + readOnlyHint?: boolean; + + /** + * If true, the tool may perform destructive updates to its environment. + * If false, the tool performs only additive updates. + * + * (This property is meaningful only when `readOnlyHint == false`) + * + * Default: true + */ + destructiveHint?: boolean; + + /** + * If true, calling the tool repeatedly with the same arguments + * will have no additional effect on its environment. + * + * (This property is meaningful only when `readOnlyHint == false`) + * + * Default: false + */ + idempotentHint?: boolean; + + /** + * If true, this tool may interact with an "open world" of external + * entities. If false, the tool's domain of interaction is closed. + * For example, the world of a web search tool is open, whereas that + * of a memory tool is not. + * + * Default: true + */ + openWorldHint?: boolean; +} + +/** + * Execution-related properties for a tool. + * + * @category `tools/list` + */ +export interface ToolExecution { + /** + * Indicates whether this tool supports task-augmented execution. + * This allows clients to handle long-running operations through polling + * the task system. + * + * - "forbidden": Tool does not support task-augmented execution (default when absent) + * - "optional": Tool may support task-augmented execution + * - "required": Tool requires task-augmented execution + * + * Default: "forbidden" + */ + taskSupport?: 'forbidden' | 'optional' | 'required'; +} + +/** + * Definition for a tool the client can call. + * + * @category `tools/list` + */ +export interface Tool extends BaseMetadata, Icons { + /** + * A human-readable description of the tool. + * + * This can be used by clients to improve the LLM's understanding of available tools. It can be thought of like a "hint" to the model. + */ + description?: string; + + /** + * A JSON Schema object defining the expected parameters for the tool. + */ + inputSchema: { + $schema?: string; + type: 'object'; + properties?: { [key: string]: object }; + required?: string[]; + }; + + /** + * Execution-related properties for this tool. + */ + execution?: ToolExecution; + + /** + * An optional JSON Schema object defining the structure of the tool's output returned in + * the structuredContent field of a CallToolResult. + * + * Defaults to JSON Schema 2020-12 when no explicit $schema is provided. + * Currently restricted to type: "object" at the root level. + */ + outputSchema?: { + $schema?: string; + type: 'object'; + properties?: { [key: string]: object }; + required?: string[]; + }; + + /** + * Optional additional tool information. + * + * Display name precedence order is: title, annotations.title, then name. + */ + annotations?: ToolAnnotations; + + /** + * See [General fields: `_meta`](/specification/2025-11-25/basic/index#meta) for notes on `_meta` usage. + */ + _meta?: { [key: string]: unknown }; +} + +/* Tasks */ + +/** + * The status of a task. + * + * @category `tasks` + */ +export type TaskStatus = + | 'working' // The request is currently being processed + | 'input_required' // The task is waiting for input (e.g., elicitation or sampling) + | 'completed' // The request completed successfully and results are available + | 'failed' // The associated request did not complete successfully. For tool calls specifically, this includes cases where the tool call result has `isError` set to true. + | 'cancelled'; // The request was cancelled before completion + +/** + * Metadata for augmenting a request with task execution. + * Include this in the `task` field of the request parameters. + * + * @category `tasks` + */ +export interface TaskMetadata { + /** + * Requested duration in milliseconds to retain task from creation. + */ + ttl?: number; +} + +/** + * Metadata for associating messages with a task. + * Include this in the `_meta` field under the key `io.modelcontextprotocol/related-task`. + * + * @category `tasks` + */ +export interface RelatedTaskMetadata { + /** + * The task identifier this message is associated with. + */ + taskId: string; +} + +/** + * Data associated with a task. + * + * @category `tasks` + */ +export interface Task { + /** + * The task identifier. + */ + taskId: string; + + /** + * Current task state. + */ + status: TaskStatus; + + /** + * Optional human-readable message describing the current task state. + * This can provide context for any status, including: + * - Reasons for "cancelled" status + * - Summaries for "completed" status + * - Diagnostic information for "failed" status (e.g., error details, what went wrong) + */ + statusMessage?: string; + + /** + * ISO 8601 timestamp when the task was created. + */ + createdAt: string; + + /** + * ISO 8601 timestamp when the task was last updated. + */ + lastUpdatedAt: string; + + /** + * Actual retention duration from creation in milliseconds, null for unlimited. + * @nullable + */ + ttl: number | null; + + /** + * Suggested polling interval in milliseconds. + */ + pollInterval?: number; +} + +/** + * A response to a task-augmented request. + * + * @category `tasks` + */ +export interface CreateTaskResult extends Result { + task: Task; +} + +/** + * A request to retrieve the state of a task. + * + * @category `tasks/get` + */ +export interface GetTaskRequest extends JSONRPCRequest { + method: 'tasks/get'; + params: { + /** + * The task identifier to query. + */ + taskId: string; + }; +} + +/** + * The response to a tasks/get request. + * + * @category `tasks/get` + */ +export type GetTaskResult = Result & Task; + +/** + * A request to retrieve the result of a completed task. + * + * @category `tasks/result` + */ +export interface GetTaskPayloadRequest extends JSONRPCRequest { + method: 'tasks/result'; + params: { + /** + * The task identifier to retrieve results for. + */ + taskId: string; + }; +} + +/** + * The response to a tasks/result request. + * The structure matches the result type of the original request. + * For example, a tools/call task would return the CallToolResult structure. + * + * @category `tasks/result` + */ +export interface GetTaskPayloadResult extends Result { + [key: string]: unknown; +} + +/** + * A request to cancel a task. + * + * @category `tasks/cancel` + */ +export interface CancelTaskRequest extends JSONRPCRequest { + method: 'tasks/cancel'; + params: { + /** + * The task identifier to cancel. + */ + taskId: string; + }; +} + +/** + * The response to a tasks/cancel request. + * + * @category `tasks/cancel` + */ +export type CancelTaskResult = Result & Task; + +/** + * A request to retrieve a list of tasks. + * + * @category `tasks/list` + */ +export interface ListTasksRequest extends PaginatedRequest { + method: 'tasks/list'; +} + +/** + * The response to a tasks/list request. + * + * @category `tasks/list` + */ +export interface ListTasksResult extends PaginatedResult { + tasks: Task[]; +} + +/** + * Parameters for a `notifications/tasks/status` notification. + * + * @category `notifications/tasks/status` + */ +export type TaskStatusNotificationParams = NotificationParams & Task; + +/** + * An optional notification from the receiver to the requestor, informing them that a task's status has changed. Receivers are not required to send these notifications. + * + * @category `notifications/tasks/status` + */ +export interface TaskStatusNotification extends JSONRPCNotification { + method: 'notifications/tasks/status'; + params: TaskStatusNotificationParams; +} + +/* Logging */ + +/** + * Parameters for a `logging/setLevel` request. + * + * @category `logging/setLevel` + */ +export interface SetLevelRequestParams extends RequestParams { + /** + * The level of logging that the client wants to receive from the server. The server should send all logs at this level and higher (i.e., more severe) to the client as notifications/message. + */ + level: LoggingLevel; +} + +/** + * A request from the client to the server, to enable or adjust logging. + * + * @category `logging/setLevel` + */ +export interface SetLevelRequest extends JSONRPCRequest { + method: 'logging/setLevel'; + params: SetLevelRequestParams; +} + +/** + * Parameters for a `notifications/message` notification. + * + * @category `notifications/message` + */ +export interface LoggingMessageNotificationParams extends NotificationParams { + /** + * The severity of this log message. + */ + level: LoggingLevel; + /** + * An optional name of the logger issuing this message. + */ + logger?: string; + /** + * The data to be logged, such as a string message or an object. Any JSON serializable type is allowed here. + */ + data: unknown; +} + +/** + * JSONRPCNotification of a log message passed from server to client. If no logging/setLevel request has been sent from the client, the server MAY decide which messages to send automatically. + * + * @category `notifications/message` + */ +export interface LoggingMessageNotification extends JSONRPCNotification { + method: 'notifications/message'; + params: LoggingMessageNotificationParams; +} + +/** + * The severity of a log message. + * + * These map to syslog message severities, as specified in RFC-5424: + * https://datatracker.ietf.org/doc/html/rfc5424#section-6.2.1 + * + * @category Common Types + */ +export type LoggingLevel = 'debug' | 'info' | 'notice' | 'warning' | 'error' | 'critical' | 'alert' | 'emergency'; + +/* Sampling */ +/** + * Parameters for a `sampling/createMessage` request. + * + * @category `sampling/createMessage` + */ +export interface CreateMessageRequestParams extends TaskAugmentedRequestParams { + messages: SamplingMessage[]; + /** + * The server's preferences for which model to select. The client MAY ignore these preferences. + */ + modelPreferences?: ModelPreferences; + /** + * An optional system prompt the server wants to use for sampling. The client MAY modify or omit this prompt. + */ + systemPrompt?: string; + /** + * A request to include context from one or more MCP servers (including the caller), to be attached to the prompt. + * The client MAY ignore this request. + * + * Default is "none". Values "thisServer" and "allServers" are soft-deprecated. Servers SHOULD only use these values if the client + * declares ClientCapabilities.sampling.context. These values may be removed in future spec releases. + */ + includeContext?: 'none' | 'thisServer' | 'allServers'; + /** + * @TJS-type number + */ + temperature?: number; + /** + * The requested maximum number of tokens to sample (to prevent runaway completions). + * + * The client MAY choose to sample fewer tokens than the requested maximum. + */ + maxTokens: number; + stopSequences?: string[]; + /** + * Optional metadata to pass through to the LLM provider. The format of this metadata is provider-specific. + */ + metadata?: object; + /** + * Tools that the model may use during generation. + * The client MUST return an error if this field is provided but ClientCapabilities.sampling.tools is not declared. + */ + tools?: Tool[]; + /** + * Controls how the model uses tools. + * The client MUST return an error if this field is provided but ClientCapabilities.sampling.tools is not declared. + * Default is `{ mode: "auto" }`. + */ + toolChoice?: ToolChoice; +} + +/** + * Controls tool selection behavior for sampling requests. + * + * @category `sampling/createMessage` + */ +export interface ToolChoice { + /** + * Controls the tool use ability of the model: + * - "auto": Model decides whether to use tools (default) + * - "required": Model MUST use at least one tool before completing + * - "none": Model MUST NOT use any tools + */ + mode?: 'auto' | 'required' | 'none'; +} + +/** + * A request from the server to sample an LLM via the client. The client has full discretion over which model to select. The client should also inform the user before beginning sampling, to allow them to inspect the request (human in the loop) and decide whether to approve it. + * + * @category `sampling/createMessage` + */ +export interface CreateMessageRequest extends JSONRPCRequest { + method: 'sampling/createMessage'; + params: CreateMessageRequestParams; +} + +/** + * The client's response to a sampling/createMessage request from the server. + * The client should inform the user before returning the sampled message, to allow them + * to inspect the response (human in the loop) and decide whether to allow the server to see it. + * + * @category `sampling/createMessage` + */ +export interface CreateMessageResult extends Result, SamplingMessage { + /** + * The name of the model that generated the message. + */ + model: string; + + /** + * The reason why sampling stopped, if known. + * + * Standard values: + * - "endTurn": Natural end of the assistant's turn + * - "stopSequence": A stop sequence was encountered + * - "maxTokens": Maximum token limit was reached + * - "toolUse": The model wants to use one or more tools + * + * This field is an open string to allow for provider-specific stop reasons. + */ + stopReason?: 'endTurn' | 'stopSequence' | 'maxTokens' | 'toolUse' | string; +} + +/** + * Describes a message issued to or received from an LLM API. + * + * @category `sampling/createMessage` + */ +export interface SamplingMessage { + role: Role; + content: SamplingMessageContentBlock | SamplingMessageContentBlock[]; + /** + * See [General fields: `_meta`](/specification/2025-11-25/basic/index#meta) for notes on `_meta` usage. + */ + _meta?: { [key: string]: unknown }; +} + +/** + * @category `sampling/createMessage` + */ +export type SamplingMessageContentBlock = TextContent | ImageContent | AudioContent | ToolUseContent | ToolResultContent; + +/** + * Optional annotations for the client. The client can use annotations to inform how objects are used or displayed + * + * @category Common Types + */ +export interface Annotations { + /** + * Describes who the intended audience of this object or data is. + * + * It can include multiple entries to indicate content useful for multiple audiences (e.g., `["user", "assistant"]`). + */ + audience?: Role[]; + + /** + * Describes how important this data is for operating the server. + * + * A value of 1 means "most important," and indicates that the data is + * effectively required, while 0 means "least important," and indicates that + * the data is entirely optional. + * + * @TJS-type number + * @minimum 0 + * @maximum 1 + */ + priority?: number; + + /** + * The moment the resource was last modified, as an ISO 8601 formatted string. + * + * Should be an ISO 8601 formatted string (e.g., "2025-01-12T15:00:58Z"). + * + * Examples: last activity timestamp in an open file, timestamp when the resource + * was attached, etc. + */ + lastModified?: string; +} + +/** + * @category Content + */ +export type ContentBlock = TextContent | ImageContent | AudioContent | ResourceLink | EmbeddedResource; + +/** + * Text provided to or from an LLM. + * + * @category Content + */ +export interface TextContent { + type: 'text'; + + /** + * The text content of the message. + */ + text: string; + + /** + * Optional annotations for the client. + */ + annotations?: Annotations; + + /** + * See [General fields: `_meta`](/specification/2025-11-25/basic/index#meta) for notes on `_meta` usage. + */ + _meta?: { [key: string]: unknown }; +} + +/** + * An image provided to or from an LLM. + * + * @category Content + */ +export interface ImageContent { + type: 'image'; + + /** + * The base64-encoded image data. + * + * @format byte + */ + data: string; + + /** + * The MIME type of the image. Different providers may support different image types. + */ + mimeType: string; + + /** + * Optional annotations for the client. + */ + annotations?: Annotations; + + /** + * See [General fields: `_meta`](/specification/2025-11-25/basic/index#meta) for notes on `_meta` usage. + */ + _meta?: { [key: string]: unknown }; +} + +/** + * Audio provided to or from an LLM. + * + * @category Content + */ +export interface AudioContent { + type: 'audio'; + + /** + * The base64-encoded audio data. + * + * @format byte + */ + data: string; + + /** + * The MIME type of the audio. Different providers may support different audio types. + */ + mimeType: string; + + /** + * Optional annotations for the client. + */ + annotations?: Annotations; + + /** + * See [General fields: `_meta`](/specification/2025-11-25/basic/index#meta) for notes on `_meta` usage. + */ + _meta?: { [key: string]: unknown }; +} + +/** + * A request from the assistant to call a tool. + * + * @category `sampling/createMessage` + */ +export interface ToolUseContent { + type: 'tool_use'; + + /** + * A unique identifier for this tool use. + * + * This ID is used to match tool results to their corresponding tool uses. + */ + id: string; + + /** + * The name of the tool to call. + */ + name: string; + + /** + * The arguments to pass to the tool, conforming to the tool's input schema. + */ + input: { [key: string]: unknown }; + + /** + * Optional metadata about the tool use. Clients SHOULD preserve this field when + * including tool uses in subsequent sampling requests to enable caching optimizations. + * + * See [General fields: `_meta`](/specification/2025-11-25/basic/index#meta) for notes on `_meta` usage. + */ + _meta?: { [key: string]: unknown }; +} + +/** + * The result of a tool use, provided by the user back to the assistant. + * + * @category `sampling/createMessage` + */ +export interface ToolResultContent { + type: 'tool_result'; + + /** + * The ID of the tool use this result corresponds to. + * + * This MUST match the ID from a previous ToolUseContent. + */ + toolUseId: string; + + /** + * The unstructured result content of the tool use. + * + * This has the same format as CallToolResult.content and can include text, images, + * audio, resource links, and embedded resources. + */ + content: ContentBlock[]; + + /** + * An optional structured result object. + * + * If the tool defined an outputSchema, this SHOULD conform to that schema. + */ + structuredContent?: { [key: string]: unknown }; + + /** + * Whether the tool use resulted in an error. + * + * If true, the content typically describes the error that occurred. + * Default: false + */ + isError?: boolean; + + /** + * Optional metadata about the tool result. Clients SHOULD preserve this field when + * including tool results in subsequent sampling requests to enable caching optimizations. + * + * See [General fields: `_meta`](/specification/2025-11-25/basic/index#meta) for notes on `_meta` usage. + */ + _meta?: { [key: string]: unknown }; +} + +/** + * The server's preferences for model selection, requested of the client during sampling. + * + * Because LLMs can vary along multiple dimensions, choosing the "best" model is + * rarely straightforward. Different models excel in different areas—some are + * faster but less capable, others are more capable but more expensive, and so + * on. This interface allows servers to express their priorities across multiple + * dimensions to help clients make an appropriate selection for their use case. + * + * These preferences are always advisory. The client MAY ignore them. It is also + * up to the client to decide how to interpret these preferences and how to + * balance them against other considerations. + * + * @category `sampling/createMessage` + */ +export interface ModelPreferences { + /** + * Optional hints to use for model selection. + * + * If multiple hints are specified, the client MUST evaluate them in order + * (such that the first match is taken). + * + * The client SHOULD prioritize these hints over the numeric priorities, but + * MAY still use the priorities to select from ambiguous matches. + */ + hints?: ModelHint[]; + + /** + * How much to prioritize cost when selecting a model. A value of 0 means cost + * is not important, while a value of 1 means cost is the most important + * factor. + * + * @TJS-type number + * @minimum 0 + * @maximum 1 + */ + costPriority?: number; + + /** + * How much to prioritize sampling speed (latency) when selecting a model. A + * value of 0 means speed is not important, while a value of 1 means speed is + * the most important factor. + * + * @TJS-type number + * @minimum 0 + * @maximum 1 + */ + speedPriority?: number; + + /** + * How much to prioritize intelligence and capabilities when selecting a + * model. A value of 0 means intelligence is not important, while a value of 1 + * means intelligence is the most important factor. + * + * @TJS-type number + * @minimum 0 + * @maximum 1 + */ + intelligencePriority?: number; +} + +/** + * Hints to use for model selection. + * + * Keys not declared here are currently left unspecified by the spec and are up + * to the client to interpret. + * + * @category `sampling/createMessage` + */ +export interface ModelHint { + /** + * A hint for a model name. + * + * The client SHOULD treat this as a substring of a model name; for example: + * - `claude-3-5-sonnet` should match `claude-3-5-sonnet-20241022` + * - `sonnet` should match `claude-3-5-sonnet-20241022`, `claude-3-sonnet-20240229`, etc. + * - `claude` should match any Claude model + * + * The client MAY also map the string to a different provider's model name or a different model family, as long as it fills a similar niche; for example: + * - `gemini-1.5-flash` could match `claude-3-haiku-20240307` + */ + name?: string; +} + +/* Autocomplete */ +/** + * Parameters for a `completion/complete` request. + * + * @category `completion/complete` + */ +export interface CompleteRequestParams extends RequestParams { + ref: PromptReference | ResourceTemplateReference; + /** + * The argument's information + */ + argument: { + /** + * The name of the argument + */ + name: string; + /** + * The value of the argument to use for completion matching. + */ + value: string; + }; + + /** + * Additional, optional context for completions + */ + context?: { + /** + * Previously-resolved variables in a URI template or prompt. + */ + arguments?: { [key: string]: string }; + }; +} + +/** + * A request from the client to the server, to ask for completion options. + * + * @category `completion/complete` + */ +export interface CompleteRequest extends JSONRPCRequest { + method: 'completion/complete'; + params: CompleteRequestParams; +} + +/** + * The server's response to a completion/complete request + * + * @category `completion/complete` + */ +export interface CompleteResult extends Result { + completion: { + /** + * An array of completion values. Must not exceed 100 items. + */ + values: string[]; + /** + * The total number of completion options available. This can exceed the number of values actually sent in the response. + */ + total?: number; + /** + * Indicates whether there are additional completion options beyond those provided in the current response, even if the exact total is unknown. + */ + hasMore?: boolean; + }; +} + +/** + * A reference to a resource or resource template definition. + * + * @category `completion/complete` + */ +export interface ResourceTemplateReference { + type: 'ref/resource'; + /** + * The URI or URI template of the resource. + * + * @format uri-template + */ + uri: string; +} + +/** + * Identifies a prompt. + * + * @category `completion/complete` + */ +export interface PromptReference extends BaseMetadata { + type: 'ref/prompt'; +} + +/* Roots */ +/** + * Sent from the server to request a list of root URIs from the client. Roots allow + * servers to ask for specific directories or files to operate on. A common example + * for roots is providing a set of repositories or directories a server should operate + * on. + * + * This request is typically used when the server needs to understand the file system + * structure or access specific locations that the client has permission to read from. + * + * @category `roots/list` + */ +export interface ListRootsRequest extends JSONRPCRequest { + method: 'roots/list'; + params?: RequestParams; +} + +/** + * The client's response to a roots/list request from the server. + * This result contains an array of Root objects, each representing a root directory + * or file that the server can operate on. + * + * @category `roots/list` + */ +export interface ListRootsResult extends Result { + roots: Root[]; +} + +/** + * Represents a root directory or file that the server can operate on. + * + * @category `roots/list` + */ +export interface Root { + /** + * The URI identifying the root. This *must* start with file:// for now. + * This restriction may be relaxed in future versions of the protocol to allow + * other URI schemes. + * + * @format uri + */ + uri: string; + /** + * An optional name for the root. This can be used to provide a human-readable + * identifier for the root, which may be useful for display purposes or for + * referencing the root in other parts of the application. + */ + name?: string; + + /** + * See [General fields: `_meta`](/specification/2025-11-25/basic/index#meta) for notes on `_meta` usage. + */ + _meta?: { [key: string]: unknown }; +} + +/** + * A notification from the client to the server, informing it that the list of roots has changed. + * This notification should be sent whenever the client adds, removes, or modifies any root. + * The server should then request an updated list of roots using the ListRootsRequest. + * + * @category `notifications/roots/list_changed` + */ +export interface RootsListChangedNotification extends JSONRPCNotification { + method: 'notifications/roots/list_changed'; + params?: NotificationParams; +} + +/** + * The parameters for a request to elicit non-sensitive information from the user via a form in the client. + * + * @category `elicitation/create` + */ +export interface ElicitRequestFormParams extends TaskAugmentedRequestParams { + /** + * The elicitation mode. + */ + mode?: 'form'; + + /** + * The message to present to the user describing what information is being requested. + */ + message: string; + + /** + * A restricted subset of JSON Schema. + * Only top-level properties are allowed, without nesting. + */ + requestedSchema: { + $schema?: string; + type: 'object'; + properties: { + [key: string]: PrimitiveSchemaDefinition; + }; + required?: string[]; + }; +} + +/** + * The parameters for a request to elicit information from the user via a URL in the client. + * + * @category `elicitation/create` + */ +export interface ElicitRequestURLParams extends TaskAugmentedRequestParams { + /** + * The elicitation mode. + */ + mode: 'url'; + + /** + * The message to present to the user explaining why the interaction is needed. + */ + message: string; + + /** + * The ID of the elicitation, which must be unique within the context of the server. + * The client MUST treat this ID as an opaque value. + */ + elicitationId: string; + + /** + * The URL that the user should navigate to. + * + * @format uri + */ + url: string; +} + +/** + * The parameters for a request to elicit additional information from the user via the client. + * + * @category `elicitation/create` + */ +export type ElicitRequestParams = ElicitRequestFormParams | ElicitRequestURLParams; + +/** + * A request from the server to elicit additional information from the user via the client. + * + * @category `elicitation/create` + */ +export interface ElicitRequest extends JSONRPCRequest { + method: 'elicitation/create'; + params: ElicitRequestParams; +} + +/** + * Restricted schema definitions that only allow primitive types + * without nested objects or arrays. + * + * @category `elicitation/create` + */ +export type PrimitiveSchemaDefinition = StringSchema | NumberSchema | BooleanSchema | EnumSchema; + +/** + * @category `elicitation/create` + */ +export interface StringSchema { + type: 'string'; + title?: string; + description?: string; + minLength?: number; + maxLength?: number; + format?: 'email' | 'uri' | 'date' | 'date-time'; + default?: string; +} + +/** + * @category `elicitation/create` + */ +export interface NumberSchema { + type: 'number' | 'integer'; + title?: string; + description?: string; + minimum?: number; + maximum?: number; + default?: number; +} + +/** + * @category `elicitation/create` + */ +export interface BooleanSchema { + type: 'boolean'; + title?: string; + description?: string; + default?: boolean; +} + +/** + * Schema for single-selection enumeration without display titles for options. + * + * @category `elicitation/create` + */ +export interface UntitledSingleSelectEnumSchema { + type: 'string'; + /** + * Optional title for the enum field. + */ + title?: string; + /** + * Optional description for the enum field. + */ + description?: string; + /** + * Array of enum values to choose from. + */ + enum: string[]; + /** + * Optional default value. + */ + default?: string; +} + +/** + * Schema for single-selection enumeration with display titles for each option. + * + * @category `elicitation/create` + */ +export interface TitledSingleSelectEnumSchema { + type: 'string'; + /** + * Optional title for the enum field. + */ + title?: string; + /** + * Optional description for the enum field. + */ + description?: string; + /** + * Array of enum options with values and display labels. + */ + oneOf: Array<{ + /** + * The enum value. + */ + const: string; + /** + * Display label for this option. + */ + title: string; + }>; + /** + * Optional default value. + */ + default?: string; +} + +/** + * @category `elicitation/create` + */ +// Combined single selection enumeration +export type SingleSelectEnumSchema = UntitledSingleSelectEnumSchema | TitledSingleSelectEnumSchema; + +/** + * Schema for multiple-selection enumeration without display titles for options. + * + * @category `elicitation/create` + */ +export interface UntitledMultiSelectEnumSchema { + type: 'array'; + /** + * Optional title for the enum field. + */ + title?: string; + /** + * Optional description for the enum field. + */ + description?: string; + /** + * Minimum number of items to select. + */ + minItems?: number; + /** + * Maximum number of items to select. + */ + maxItems?: number; + /** + * Schema for the array items. + */ + items: { + type: 'string'; + /** + * Array of enum values to choose from. + */ + enum: string[]; + }; + /** + * Optional default value. + */ + default?: string[]; +} + +/** + * Schema for multiple-selection enumeration with display titles for each option. + * + * @category `elicitation/create` + */ +export interface TitledMultiSelectEnumSchema { + type: 'array'; + /** + * Optional title for the enum field. + */ + title?: string; + /** + * Optional description for the enum field. + */ + description?: string; + /** + * Minimum number of items to select. + */ + minItems?: number; + /** + * Maximum number of items to select. + */ + maxItems?: number; + /** + * Schema for array items with enum options and display labels. + */ + items: { + /** + * Array of enum options with values and display labels. + */ + anyOf: Array<{ + /** + * The constant enum value. + */ + const: string; + /** + * Display title for this option. + */ + title: string; + }>; + }; + /** + * Optional default value. + */ + default?: string[]; +} + +/** + * @category `elicitation/create` + */ +// Combined multiple selection enumeration +export type MultiSelectEnumSchema = UntitledMultiSelectEnumSchema | TitledMultiSelectEnumSchema; + +/** + * Use TitledSingleSelectEnumSchema instead. + * This interface will be removed in a future version. + * + * @category `elicitation/create` + */ +export interface LegacyTitledEnumSchema { + type: 'string'; + title?: string; + description?: string; + enum: string[]; + /** + * (Legacy) Display names for enum values. + * Non-standard according to JSON schema 2020-12. + */ + enumNames?: string[]; + default?: string; +} + +/** + * @category `elicitation/create` + */ +// Union type for all enum schemas +export type EnumSchema = SingleSelectEnumSchema | MultiSelectEnumSchema | LegacyTitledEnumSchema; + +/** + * The client's response to an elicitation request. + * + * @category `elicitation/create` + */ +export interface ElicitResult extends Result { + /** + * The user action in response to the elicitation. + * - "accept": User submitted the form/confirmed the action + * - "decline": User explicitly decline the action + * - "cancel": User dismissed without making an explicit choice + */ + action: 'accept' | 'decline' | 'cancel'; + + /** + * The submitted form data, only present when action is "accept" and mode was "form". + * Contains values matching the requested schema. + * Omitted for out-of-band mode responses. + */ + content?: { [key: string]: string | number | boolean | string[] }; +} + +/** + * An optional notification from the server to the client, informing it of a completion of a out-of-band elicitation request. + * + * @category `notifications/elicitation/complete` + */ +export interface ElicitationCompleteNotification extends JSONRPCNotification { + method: 'notifications/elicitation/complete'; + params: { + /** + * The ID of the elicitation that completed. + */ + elicitationId: string; + }; +} + +/* Client messages */ +/** @internal */ +export type ClientRequest = + | PingRequest + | InitializeRequest + | CompleteRequest + | SetLevelRequest + | GetPromptRequest + | ListPromptsRequest + | ListResourcesRequest + | ListResourceTemplatesRequest + | ReadResourceRequest + | SubscribeRequest + | UnsubscribeRequest + | CallToolRequest + | ListToolsRequest + | GetTaskRequest + | GetTaskPayloadRequest + | ListTasksRequest + | CancelTaskRequest; + +/** @internal */ +export type ClientNotification = + | CancelledNotification + | ProgressNotification + | InitializedNotification + | RootsListChangedNotification + | TaskStatusNotification; + +/** @internal */ +export type ClientResult = + | EmptyResult + | CreateMessageResult + | ListRootsResult + | ElicitResult + | GetTaskResult + | GetTaskPayloadResult + | ListTasksResult + | CancelTaskResult; + +/* Server messages */ +/** @internal */ +export type ServerRequest = + | PingRequest + | CreateMessageRequest + | ListRootsRequest + | ElicitRequest + | GetTaskRequest + | GetTaskPayloadRequest + | ListTasksRequest + | CancelTaskRequest; + +/** @internal */ +export type ServerNotification = + | CancelledNotification + | ProgressNotification + | LoggingMessageNotification + | ResourceUpdatedNotification + | ResourceListChangedNotification + | ToolListChangedNotification + | PromptListChangedNotification + | ElicitationCompleteNotification + | TaskStatusNotification; + +/** @internal */ +export type ServerResult = + | EmptyResult + | InitializeResult + | CompleteResult + | GetPromptResult + | ListPromptsResult + | ListResourceTemplatesResult + | ListResourcesResult + | ReadResourceResult + | CallToolResult + | ListToolsResult + | GetTaskResult + | GetTaskPayloadResult + | ListTasksResult + | CancelTaskResult; diff --git a/packages/core-internal/src/types/spec.types.2026-07-28.ts b/packages/core-internal/src/types/spec.types.2026-07-28.ts new file mode 100644 index 0000000000..7305df0462 --- /dev/null +++ b/packages/core-internal/src/types/spec.types.2026-07-28.ts @@ -0,0 +1,3030 @@ +/** + * This file is automatically generated from the Model Context Protocol specification. + * + * Source: https://github.com/modelcontextprotocol/modelcontextprotocol + * Pulled from: https://raw.githubusercontent.com/modelcontextprotocol/modelcontextprotocol/main/schema/draft/schema.ts + * Last updated from commit: 9d700ed62dcf86cb77475c9b81930611a9182f46 + * + * DO NOT EDIT THIS FILE MANUALLY. Changes will be overwritten by automated updates. + * To update this file, run: pnpm run fetch:spec-types 2026-07-28 + */ /* JSON types */ + +/** + * @category Common Types + */ +export type JSONValue = string | number | boolean | null | JSONObject | JSONArray; + +/** + * @category Common Types + */ +export type JSONObject = { [key: string]: JSONValue }; + +/** + * @category Common Types + */ +export type JSONArray = JSONValue[]; + +/* JSON-RPC types */ + +/** + * Refers to any valid JSON-RPC object that can be decoded off the wire, or encoded to be sent. + * + * @category JSON-RPC + */ +export type JSONRPCMessage = JSONRPCRequest | JSONRPCNotification | JSONRPCResponse; + +/** @internal */ +export const LATEST_PROTOCOL_VERSION = '2026-07-28'; +/** @internal */ +export const JSONRPC_VERSION = '2.0'; + +/** + * Represents the contents of a `_meta` field, which clients and servers use to attach additional metadata to their interactions. + * + * Certain key names are reserved by MCP for protocol-level metadata; implementations MUST NOT make assumptions about values at these keys. Additionally, specific schema definitions may reserve particular names for purpose-specific metadata, as declared in those definitions. + * + * Valid keys have two segments: + * + * **Prefix:** + * - Optional — if specified, MUST be a series of _labels_ separated by dots (`.`), followed by a slash (`/`). + * - Labels MUST start with a letter and end with a letter or digit. Interior characters may be letters, digits, or hyphens (`-`). + * - Implementations SHOULD use reverse DNS notation (e.g., `com.example/` rather than `example.com/`). + * - Any prefix where the second label is `modelcontextprotocol` or `mcp` is **reserved** for MCP use. For example: `io.modelcontextprotocol/`, `dev.mcp/`, `org.modelcontextprotocol.api/`, and `com.mcp.tools/` are all reserved. However, `com.example.mcp/` is NOT reserved, as the second label is `example`. + * + * **Name:** + * - Unless empty, MUST start and end with an alphanumeric character (`[a-z0-9A-Z]`). + * - Interior characters may be alphanumeric, hyphens (`-`), underscores (`_`), or dots (`.`). + * + * @see [General fields: `_meta`](/specification/draft/basic/index#meta) for more details. + * @category Common Types + */ +export type MetaObject = Record; + +/** + * Extends {@link MetaObject} with additional request-specific fields. All key naming rules from `MetaObject` apply. + * + * @see {@link MetaObject} for key naming rules and reserved prefixes. + * @see [General fields: `_meta`](/specification/draft/basic/index#meta) for more details. + * @category Common Types + */ +export interface RequestMetaObject extends MetaObject { + /** + * If specified, the caller is requesting out-of-band progress notifications for this request (as represented by {@link ProgressNotification | notifications/progress}). The value of this parameter is an opaque token that will be attached to any subsequent notifications. The receiver is not obligated to provide these notifications. + */ + progressToken?: ProgressToken; + /** + * The MCP Protocol Version being used for this request. Required. + * + * For the HTTP transport, this value MUST match the `MCP-Protocol-Version` + * header; otherwise the server MUST return a `400 Bad Request`. If the + * server does not support the requested version, it MUST return an + * {@link UnsupportedProtocolVersionError}. + */ + 'io.modelcontextprotocol/protocolVersion': string; + /** + * Identifies the client software making the request. Required. + * + * The {@link Implementation} schema requires `name` and `version`; other + * fields are optional. + */ + 'io.modelcontextprotocol/clientInfo': Implementation; + /** + * The client's capabilities for this specific request. Required. + * + * Capabilities are declared per-request rather than once at initialization; + * an empty object means the client supports no optional capabilities. + * Servers MUST NOT infer capabilities from prior requests. + */ + 'io.modelcontextprotocol/clientCapabilities': ClientCapabilities; + /** + * The desired log level for this request. Optional. + * + * If absent, the server MUST NOT send any {@link LoggingMessageNotification | notifications/message} + * notifications for this request. The client opts in to log messages by + * explicitly setting a level. Replaces the former `logging/setLevel` RPC. + * + * @deprecated Deprecated as of protocol version 2026-07-28 (SEP-2577). + * Remains in the specification for at least twelve months; see the + * deprecated features registry. + */ + 'io.modelcontextprotocol/logLevel'?: LoggingLevel; +} + +/** + * A progress token, used to associate progress notifications with the original request. + * + * @category Common Types + */ +export type ProgressToken = string | number; + +/** + * An opaque token used to represent a cursor for pagination. + * + * @category Common Types + */ +export type Cursor = string; + +/** + * Common params for any request. + * + * @category Common Types + */ +export interface RequestParams { + _meta: RequestMetaObject; +} + +/** @internal */ +export interface Request { + method: string; + // Allow unofficial extensions of `Request.params` without impacting `RequestParams`. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + params?: { [key: string]: any }; +} + +/** + * Common params for any notification. + * + * @category Common Types + */ +export interface NotificationParams { + _meta?: MetaObject; +} + +/** @internal */ +export interface Notification { + method: string; + // Allow unofficial extensions of `Notification.params` without impacting `NotificationParams`. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + params?: { [key: string]: any }; +} + +/** + * Indicates the type of a {@link Result} object, allowing the client to + * determine how to parse the response. + * + * complete - the request completed successfully and the result contains the final content. + * input_required - the request requires additional input and the result contains an {@link InputRequiredResult} object with instructions for the client to provide additional input before retrying the original request. + * @category Common Types + */ +export type ResultType = 'complete' | 'input_required' | string; + +/** + * Common result fields. + * + * @category Common Types + */ +export interface Result { + _meta?: MetaObject; + /** + * Indicates the type of the result, which allows the client to determine + * how to parse the result object. + * + * Servers implementing this protocol version MUST include this field. + * For backward compatibility, when a client receives a result from a + * server implementing an earlier protocol version (which does not include + * `resultType`), the client MUST treat the absent field as `"complete"`. + */ + resultType: ResultType; + [key: string]: unknown; +} + +/** + * @category Errors + */ +export interface Error { + /** + * The error type that occurred. + */ + code: number; + /** + * A short description of the error. The message SHOULD be limited to a concise single sentence. + */ + message: string; + /** + * Additional information about the error. The value of this member is defined by the sender (e.g. detailed error information, nested errors etc.). + */ + data?: unknown; +} + +/** + * A uniquely identifying ID for a request in JSON-RPC. + * + * @category Common Types + */ +export type RequestId = string | number; + +/** + * A request that expects a response. + * + * @category JSON-RPC + */ +export interface JSONRPCRequest extends Request { + jsonrpc: typeof JSONRPC_VERSION; + id: RequestId; +} + +/** + * A notification which does not expect a response. + * + * @category JSON-RPC + */ +export interface JSONRPCNotification extends Notification { + jsonrpc: typeof JSONRPC_VERSION; +} + +/** + * A successful (non-error) response to a request. + * + * @category JSON-RPC + */ +export interface JSONRPCResultResponse { + jsonrpc: typeof JSONRPC_VERSION; + id: RequestId; + result: Result; +} + +/** + * A response to a request that indicates an error occurred. + * + * @category JSON-RPC + */ +export interface JSONRPCErrorResponse { + jsonrpc: typeof JSONRPC_VERSION; + id?: RequestId; + error: Error; +} + +/** + * A response to a request, containing either the result or error. + * + * @category JSON-RPC + */ +export type JSONRPCResponse = JSONRPCResultResponse | JSONRPCErrorResponse; + +// Standard JSON-RPC error codes +export const PARSE_ERROR = -32700; +export const INVALID_REQUEST = -32600; +export const METHOD_NOT_FOUND = -32601; +export const INVALID_PARAMS = -32602; +export const INTERNAL_ERROR = -32603; + +/** + * A JSON-RPC error indicating that invalid JSON was received by the server. This error is returned when the server cannot parse the JSON text of a message. + * + * @see {@link https://www.jsonrpc.org/specification#error_object | JSON-RPC 2.0 Error Object} + * + * @example Invalid JSON + * {@includeCode ./examples/ParseError/invalid-json.json} + * + * @category Errors + */ +export interface ParseError extends Error { + code: typeof PARSE_ERROR; +} + +/** + * A JSON-RPC error indicating that the request is not a valid request object. This error is returned when the message structure does not conform to the JSON-RPC 2.0 specification requirements for a request (e.g., missing required fields like `jsonrpc` or `method`, or using invalid types for these fields). + * + * @see {@link https://www.jsonrpc.org/specification#error_object | JSON-RPC 2.0 Error Object} + * + * @category Errors + */ +export interface InvalidRequestError extends Error { + code: typeof INVALID_REQUEST; +} + +/** + * A JSON-RPC error indicating that the requested method does not exist or is not available. + * + * In MCP, a server returns this error when a client invokes a method the server does not implement — either a genuinely unknown method, or one gated behind a server capability the server did not advertise (e.g., calling `prompts/list` when the `prompts` capability was not advertised). + * + * A request that requires a client capability the client did not declare is signalled instead by {@link MissingRequiredClientCapabilityError} (`-32003`). + * + * @see {@link https://www.jsonrpc.org/specification#error_object | JSON-RPC 2.0 Error Object} + * + * @example Prompts not supported + * {@includeCode ./examples/MethodNotFoundError/prompts-not-supported.json} + * + * @category Errors + */ +export interface MethodNotFoundError extends Error { + code: typeof METHOD_NOT_FOUND; +} + +/** + * A JSON-RPC error indicating that the method parameters are invalid or malformed. + * + * In MCP, this error is returned in various contexts when request parameters fail validation: + * + * - **Tools**: Unknown tool name or invalid tool arguments + * - **Prompts**: Unknown prompt name or missing required arguments + * - **Pagination**: Invalid or expired cursor values + * - **Logging**: Invalid log level + * - **Elicitation**: Server requests an elicitation mode not declared in client capabilities + * - **Sampling**: Missing tool result or tool results mixed with other content + * + * @see {@link https://www.jsonrpc.org/specification#error_object | JSON-RPC 2.0 Error Object} + * + * @example Unknown tool + * {@includeCode ./examples/InvalidParamsError/unknown-tool.json} + * + * @example Invalid tool arguments + * {@includeCode ./examples/InvalidParamsError/invalid-tool-arguments.json} + * + * @example Unknown prompt + * {@includeCode ./examples/InvalidParamsError/unknown-prompt.json} + * + * @example Invalid cursor + * {@includeCode ./examples/InvalidParamsError/invalid-cursor.json} + * + * @category Errors + */ +export interface InvalidParamsError extends Error { + code: typeof INVALID_PARAMS; +} + +/** + * A JSON-RPC error indicating that an internal error occurred on the receiver. This error is returned when the receiver encounters an unexpected condition that prevents it from fulfilling the request. + * + * @see {@link https://www.jsonrpc.org/specification#error_object | JSON-RPC 2.0 Error Object} + * + * @example Unexpected error + * {@includeCode ./examples/InternalError/unexpected-error.json} + * + * @category Errors + */ +export interface InternalError extends Error { + code: typeof INTERNAL_ERROR; +} + +/** + * Error code returned when a server requires a client capability that was + * not declared in the request's `clientCapabilities`. + * + * @category Errors + */ +export const MISSING_REQUIRED_CLIENT_CAPABILITY = -32003; + +/** + * Error code returned when the request's protocol version is not supported + * by the server. + * + * @category Errors + */ +export const UNSUPPORTED_PROTOCOL_VERSION = -32004; + +/** + * Returned when the request's protocol version is unknown to the server or + * unsupported (e.g., a known experimental or draft version the server has + * chosen not to implement). For HTTP, the response status code MUST be + * `400 Bad Request`. + * + * @example Unsupported protocol version + * {@includeCode ./examples/UnsupportedProtocolVersionError/unsupported-version.json} + * + * @category Errors + */ +export interface UnsupportedProtocolVersionError extends Omit { + error: Error & { + code: typeof UNSUPPORTED_PROTOCOL_VERSION; + data: { + /** + * Protocol versions the server supports. The client should choose a + * mutually supported version from this list and retry. + */ + supported: string[]; + /** + * The protocol version that was requested by the client. + */ + requested: string; + }; + }; +} + +/** + * Returned when processing a request requires a capability the client did not + * declare in `clientCapabilities`. For HTTP, the response status code MUST be + * `400 Bad Request`. + * + * @example Missing elicitation capability + * {@includeCode ./examples/MissingRequiredClientCapabilityError/missing-elicitation-capability.json} + * + * @category Errors + */ +export interface MissingRequiredClientCapabilityError extends Omit { + error: Error & { + code: typeof MISSING_REQUIRED_CLIENT_CAPABILITY; + data: { + /** + * The capabilities the server requires from the client to process this request. + */ + requiredCapabilities: ClientCapabilities; + }; + }; +} + +/* Empty result */ +/** + * A result that indicates success but carries no data. + * + * @category Common Types + */ +export type EmptyResult = Result; + +/** @internal */ +export type InputRequest = CreateMessageRequest | ListRootsRequest | ElicitRequest; + +/** @internal */ +export type InputResponse = CreateMessageResult | ListRootsResult | ElicitResult; + +/** + * A map of server-initiated requests that the client must fulfill. + * Keys are server-assigned identifiers; values are the request objects. + * + * @example Elicitation and sampling input requests + * {@includeCode ./examples/InputRequests/elicitation-and-sampling-input-requests.json} + * + * @category Multi Round-Trip + */ +export interface InputRequests { + [key: string]: InputRequest; +} + +/** + * A map of client responses to server-initiated requests. + * Keys correspond to the keys in the {@link InputRequests} map; + * values are the client's result for each request. + * + * @example Elicitation and sampling input responses + * {@includeCode ./examples/InputResponses/elicitation-and-sampling-input-responses.json} + * + * @category Multi Round-Trip + */ +export interface InputResponses { + [key: string]: InputResponse; +} + +/** + * An InputRequiredResult sent by the server to indicate that additional input is needed + * before the request can be completed. + * + * At least one of `inputRequests` or `requestState` MUST be present. + * @example InputRequiredResult with elicitation and sampling input requests and request state + * {@includeCode ./examples/InputRequiredResult/input-required-result-with-elicitation-and-sampling-and-request-state.json} + * + * @example InputRequiredResult with request state only (load shedding) + * {@includeCode ./examples/InputRequiredResult/input-required-result-with-request-state-only.json} + * + * @category Multi Round-Trip + */ +export interface InputRequiredResult extends Result { + /* Requests issued by the server that must be complete before the + * client can retry the original request. + */ + inputRequests?: InputRequests; + /* Request state to be passed back to the server when the client + * retries the original request. + * Note: The client must treat this as an opaque blob; it must not + * interpret it in any way. + */ + requestState?: string; +} + +/* Request parameter type that includes input responses and request state. + * These parameters may be included in any client-initiated request. + */ +export interface InputResponseRequestParams extends RequestParams { + /* New field to carry the responses for the server's requests from the + * InputRequiredResult message. For each key in the response's inputRequests + * field, the same key must appear here with the associated response. + */ + inputResponses?: InputResponses; + /* Request state passed back to the server from the client. + */ + requestState?: string; +} + +/* Cancellation */ +/** + * Parameters for a `notifications/cancelled` notification. + * + * @example User-requested cancellation + * {@includeCode ./examples/CancelledNotificationParams/user-requested-cancellation.json} + * + * @category `notifications/cancelled` + */ +export interface CancelledNotificationParams extends NotificationParams { + /** + * The ID of the request to cancel. + * + * This MUST correspond to the ID of a request previously issued in the same direction. + */ + requestId?: RequestId; + + /** + * An optional string describing the reason for the cancellation. This MAY be logged or presented to the user. + */ + reason?: string; +} + +/** + * This notification can be sent by either side to indicate that it is cancelling a previously-issued request. + * + * The request SHOULD still be in-flight, but due to communication latency, it is always possible that this notification MAY arrive after the request has already finished. + * + * This notification indicates that the result will be unused, so any associated processing SHOULD cease. + * + * @example User-requested cancellation + * {@includeCode ./examples/CancelledNotification/user-requested-cancellation.json} + * + * @category `notifications/cancelled` + */ +export interface CancelledNotification extends JSONRPCNotification { + method: 'notifications/cancelled'; + params: CancelledNotificationParams; +} + +/* Discovery */ +/** + * A request from the client asking the server to advertise its supported + * protocol versions, capabilities, and other metadata. Servers **MUST** + * implement `server/discover`. Clients **MAY** call it but are not required + * to — version negotiation can also happen inline via per-request `_meta`. + * + * @example Discover request + * {@includeCode ./examples/DiscoverRequest/server-discover-request.json} + * + * @category `server/discover` + */ +export interface DiscoverRequest extends JSONRPCRequest { + method: 'server/discover'; + params: RequestParams; +} + +/** + * The result returned by the server for a {@link DiscoverRequest | server/discover} request. + * + * @example Server capabilities discovery + * {@includeCode ./examples/DiscoverResult/server-capabilities-discovery.json} + * + * @category `server/discover` + */ +export interface DiscoverResult extends Result { + /** + * MCP Protocol Versions this server supports. The client should choose a + * version from this list for use in subsequent requests. + */ + supportedVersions: string[]; + /** + * The capabilities of the server. + */ + capabilities: ServerCapabilities; + /** + * Information about the server software implementation. + */ + serverInfo: Implementation; + /** + * Natural-language guidance describing the server and its features. + * + * This can be used by clients to improve an LLM's understanding of + * available tools (e.g., by including it in a system prompt). It should + * focus on information that helps the model use the server effectively + * and should not duplicate information already in tool descriptions. + */ + instructions?: string; +} + +/** + * A successful response from the server for a {@link DiscoverRequest | server/discover} request. + * + * @example Discover result response + * {@includeCode ./examples/DiscoverResultResponse/discover-result-response.json} + * + * @category `server/discover` + */ +export interface DiscoverResultResponse extends JSONRPCResultResponse { + result: DiscoverResult; +} + +/** + * Capabilities a client may support. Known capabilities are defined here, in this schema, but this is not a closed set: any client can define its own, additional capabilities. + * + * @category `server/discover` + */ +export interface ClientCapabilities { + /** + * Experimental, non-standard capabilities that the client supports. + */ + experimental?: { [key: string]: JSONObject }; + /** + * Present if the client supports listing roots. + * + * @deprecated Deprecated as of protocol version 2026-07-28 (SEP-2577). + * Remains in the specification for at least twelve months; see the + * deprecated features registry. + * + * @example Roots — minimum baseline support + * {@includeCode ./examples/ClientCapabilities/roots-minimum-baseline-support.json} + */ + // eslint-disable-next-line @typescript-eslint/no-empty-object-type + roots?: {}; + /** + * Present if the client supports sampling from an LLM. + * + * @deprecated Deprecated as of protocol version 2026-07-28 (SEP-2577). + * Remains in the specification for at least twelve months; see the + * deprecated features registry. + * + * @example Sampling — minimum baseline support + * {@includeCode ./examples/ClientCapabilities/sampling-minimum-baseline-support.json} + * + * @example Sampling — tool use support + * {@includeCode ./examples/ClientCapabilities/sampling-tool-use-support.json} + * + * @example Sampling — context inclusion support (deprecated) + * {@includeCode ./examples/ClientCapabilities/sampling-context-inclusion-support-deprecated.json} + */ + sampling?: { + /** + * Whether the client supports context inclusion via `includeContext` parameter. + * If not declared, servers SHOULD only use `includeContext: "none"` (or omit it). + */ + context?: JSONObject; + /** + * Whether the client supports tool use via `tools` and `toolChoice` parameters. + */ + tools?: JSONObject; + }; + /** + * Present if the client supports elicitation from the server. + * + * @example Elicitation — form and URL mode support + * {@includeCode ./examples/ClientCapabilities/elicitation-form-and-url-mode-support.json} + * + * @example Elicitation — form mode only (implicit) + * {@includeCode ./examples/ClientCapabilities/elicitation-form-only-implicit.json} + */ + elicitation?: { + form?: JSONObject; + url?: JSONObject; + }; + + /** + * Optional MCP extensions that the client supports. Keys are extension identifiers + * (e.g., "io.modelcontextprotocol/oauth-client-credentials"), and values are + * per-extension settings objects. An empty object indicates support with no settings. + * + * @example Extensions — MCP Apps (UI) extension with MIME type support + * {@includeCode ./examples/ClientCapabilities/extensions-ui-mime-types.json} + */ + extensions?: { [key: string]: JSONObject }; +} + +/** + * Capabilities that a server may support. Known capabilities are defined here, in this schema, but this is not a closed set: any server can define its own, additional capabilities. + * + * @category `server/discover` + */ +export interface ServerCapabilities { + /** + * Experimental, non-standard capabilities that the server supports. + */ + experimental?: { [key: string]: JSONObject }; + /** + * Present if the server supports sending log messages to the client. + * + * @deprecated Deprecated as of protocol version 2026-07-28 (SEP-2577). + * Remains in the specification for at least twelve months; see the + * deprecated features registry. + * + * @example Logging — minimum baseline support + * {@includeCode ./examples/ServerCapabilities/logging-minimum-baseline-support.json} + */ + logging?: JSONObject; + /** + * Present if the server supports argument autocompletion suggestions. + * + * @example Completions — minimum baseline support + * {@includeCode ./examples/ServerCapabilities/completions-minimum-baseline-support.json} + */ + completions?: JSONObject; + /** + * Present if the server offers any prompt templates. + * + * @example Prompts — minimum baseline support + * {@includeCode ./examples/ServerCapabilities/prompts-minimum-baseline-support.json} + * + * @example Prompts — list changed notifications + * {@includeCode ./examples/ServerCapabilities/prompts-list-changed-notifications.json} + */ + prompts?: { + /** + * Whether this server supports notifications for changes to the prompt list. + */ + listChanged?: boolean; + }; + /** + * Present if the server offers any resources to read. + * + * @example Resources — minimum baseline support + * {@includeCode ./examples/ServerCapabilities/resources-minimum-baseline-support.json} + * + * @example Resources — subscription to individual resource updates (only) + * {@includeCode ./examples/ServerCapabilities/resources-subscription-to-individual-resource-updates-only.json} + * + * @example Resources — list changed notifications (only) + * {@includeCode ./examples/ServerCapabilities/resources-list-changed-notifications-only.json} + * + * @example Resources — all notifications + * {@includeCode ./examples/ServerCapabilities/resources-all-notifications.json} + */ + resources?: { + /** + * Whether this server supports subscribing to resource updates. + */ + subscribe?: boolean; + /** + * Whether this server supports notifications for changes to the resource list. + */ + listChanged?: boolean; + }; + /** + * Present if the server offers any tools to call. + * + * @example Tools — minimum baseline support + * {@includeCode ./examples/ServerCapabilities/tools-minimum-baseline-support.json} + * + * @example Tools — list changed notifications + * {@includeCode ./examples/ServerCapabilities/tools-list-changed-notifications.json} + */ + tools?: { + /** + * Whether this server supports notifications for changes to the tool list. + */ + listChanged?: boolean; + }; + /** + * Optional MCP extensions that the server supports. Keys are extension identifiers + * (e.g., "io.modelcontextprotocol/tasks"), and values are per-extension settings + * objects. An empty object indicates support with no settings. + * + * @example Extensions — Tasks extension support + * {@includeCode ./examples/ServerCapabilities/extensions-tasks.json} + */ + extensions?: { [key: string]: JSONObject }; +} + +/** + * An optionally-sized icon that can be displayed in a user interface. + * + * @category Common Types + */ +export interface Icon { + /** + * A standard URI pointing to an icon resource. May be an HTTP/HTTPS URL or a + * `data:` URI with Base64-encoded image data. + * + * Consumers SHOULD take steps to ensure URLs serving icons are from the + * same domain as the client/server or a trusted domain. + * + * Consumers SHOULD take appropriate precautions when consuming SVGs as they can contain + * executable JavaScript. + * + * @format uri + */ + src: string; + + /** + * Optional MIME type override if the source MIME type is missing or generic. + * For example: `"image/png"`, `"image/jpeg"`, or `"image/svg+xml"`. + */ + mimeType?: string; + + /** + * Optional array of strings that specify sizes at which the icon can be used. + * Each string should be in WxH format (e.g., `"48x48"`, `"96x96"`) or `"any"` for scalable formats like SVG. + * + * If not provided, the client should assume that the icon can be used at any size. + */ + sizes?: string[]; + + /** + * Optional specifier for the theme this icon is designed for. `"light"` indicates + * the icon is designed to be used with a light background, and `"dark"` indicates + * the icon is designed to be used with a dark background. + * + * If not provided, the client should assume the icon can be used with any theme. + */ + theme?: 'light' | 'dark'; +} + +/** + * Base interface to add `icons` property. + * + * @internal + */ +export interface Icons { + /** + * Optional set of sized icons that the client can display in a user interface. + * + * Clients that support rendering icons MUST support at least the following MIME types: + * - `image/png` - PNG images (safe, universal compatibility) + * - `image/jpeg` (and `image/jpg`) - JPEG images (safe, universal compatibility) + * + * Clients that support rendering icons SHOULD also support: + * - `image/svg+xml` - SVG images (scalable but requires security precautions) + * - `image/webp` - WebP images (modern, efficient format) + */ + icons?: Icon[]; +} + +/** + * Base interface for metadata with name (identifier) and title (display name) properties. + * + * @internal + */ +export interface BaseMetadata { + /** + * Intended for programmatic or logical use, but used as a display name in past specs or fallback (if title isn't present). + */ + name: string; + + /** + * Intended for UI and end-user contexts — optimized to be human-readable and easily understood, + * even by those unfamiliar with domain-specific terminology. + * + * If not provided, the name should be used for display (except for {@link Tool}, + * where `annotations.title` should be given precedence over using `name`, + * if present). + */ + title?: string; +} + +/** + * Describes the MCP implementation. + * + * @category `server/discover` + */ +export interface Implementation extends BaseMetadata, Icons { + /** + * The version of this implementation. + */ + version: string; + + /** + * An optional human-readable description of what this implementation does. + * + * This can be used by clients or servers to provide context about their purpose + * and capabilities. For example, a server might describe the types of resources + * or tools it provides, while a client might describe its intended use case. + */ + description?: string; + + /** + * An optional URL of the website for this implementation. + * + * @format uri + */ + websiteUrl?: string; +} + +/* Progress notifications */ + +/** + * Parameters for a {@link ProgressNotification | notifications/progress} notification. + * + * @example Progress message + * {@includeCode ./examples/ProgressNotificationParams/progress-message.json} + * + * @category `notifications/progress` + */ +export interface ProgressNotificationParams extends NotificationParams { + /** + * The progress token which was given in the initial request, used to associate this notification with the request that is proceeding. + */ + progressToken: ProgressToken; + /** + * The progress thus far. This should increase every time progress is made, even if the total is unknown. + * + * @TJS-type number + */ + progress: number; + /** + * Total number of items to process (or total progress required), if known. + * + * @TJS-type number + */ + total?: number; + /** + * An optional message describing the current progress. + */ + message?: string; +} + +/** + * An out-of-band notification used to inform the receiver of a progress update for a long-running request. + * + * @example Progress message + * {@includeCode ./examples/ProgressNotification/progress-message.json} + * + * @category `notifications/progress` + */ +export interface ProgressNotification extends JSONRPCNotification { + method: 'notifications/progress'; + params: ProgressNotificationParams; +} + +/* Pagination */ +/** + * Common params for paginated requests. + * + * @example List request with cursor + * {@includeCode ./examples/PaginatedRequestParams/list-with-cursor.json} + * + * @category Common Types + */ +export interface PaginatedRequestParams extends RequestParams { + /** + * An opaque token representing the current pagination position. + * If provided, the server should return results starting after this cursor. + */ + cursor?: Cursor; +} + +/** @internal */ +export interface PaginatedRequest extends JSONRPCRequest { + params: PaginatedRequestParams; +} + +/** @internal */ +export interface PaginatedResult extends Result { + /** + * An opaque token representing the pagination position after the last returned result. + * If present, there may be more results available. + */ + nextCursor?: Cursor; +} + +/** + * A result that supports a time-to-live (TTL) hint for client-side caching. + * + * @internal + */ +export interface CacheableResult extends Result { + /** + * A hint from the server indicating how long (in milliseconds) the + * client MAY cache this response before re-fetching. Semantics are + * analogous to HTTP Cache-Control max-age. + * + * - If 0, The response SHOULD be considered immediately stale, + * The client MAY re-fetch every time the result is needed. + * - If positive, the client SHOULD consider the result fresh for this many + * milliseconds after receiving the response. + * + * @minimum 0 + */ + ttlMs: number; + + /** + * Indicates the intended scope of the cached response, analogous to HTTP + * `Cache-Control: public` vs `Cache-Control: private`. + * + * - `"public"`: Any client or intermediary (e.g., shared gateway, proxy) + * MAY cache the response and serve it to any user. + * - `"private"`: Only the requesting user's client MAY cache the response. + * Shared caches (e.g., multi-tenant gateways) MUST NOT serve a cached + * copy to a different user. + * + */ + cacheScope: 'public' | 'private'; +} + +/* Resources */ +/** + * Sent from the client to request a list of resources the server has. + * + * @example List resources request + * {@includeCode ./examples/ListResourcesRequest/list-resources-request.json} + * + * @category `resources/list` + */ +export interface ListResourcesRequest extends PaginatedRequest { + method: 'resources/list'; +} + +/** + * The result returned by the server for a {@link ListResourcesRequest | resources/list} request. + * + * @example Resources list with cursor and TTL + * {@includeCode ./examples/ListResourcesResult/resources-list-with-cursor-and-ttl.json} + * + * @category `resources/list` + */ +export interface ListResourcesResult extends PaginatedResult, CacheableResult { + resources: Resource[]; +} + +/** + * A successful response from the server for a {@link ListResourcesRequest | resources/list} request. + * + * @example List resources result response + * {@includeCode ./examples/ListResourcesResultResponse/list-resources-result-response.json} + * + * @category `resources/list` + */ +export interface ListResourcesResultResponse extends JSONRPCResultResponse { + result: ListResourcesResult; +} + +/** + * Sent from the client to request a list of resource templates the server has. + * + * @example List resource templates request + * {@includeCode ./examples/ListResourceTemplatesRequest/list-resource-templates-request.json} + * + * @category `resources/templates/list` + */ +export interface ListResourceTemplatesRequest extends PaginatedRequest { + method: 'resources/templates/list'; +} + +/** + * The result returned by the server for a {@link ListResourceTemplatesRequest | resources/templates/list} request. + * + * @example Resource templates list with cursor and TTL + * {@includeCode ./examples/ListResourceTemplatesResult/resource-templates-list-with-cursor-and-ttl.json} + * + * @category `resources/templates/list` + */ +export interface ListResourceTemplatesResult extends PaginatedResult, CacheableResult { + resourceTemplates: ResourceTemplate[]; +} + +/** + * A successful response from the server for a {@link ListResourceTemplatesRequest | resources/templates/list} request. + * + * @example List resource templates result response + * {@includeCode ./examples/ListResourceTemplatesResultResponse/list-resource-templates-result-response.json} + * + * @category `resources/templates/list` + */ +export interface ListResourceTemplatesResultResponse extends JSONRPCResultResponse { + result: ListResourceTemplatesResult; +} + +/** + * Common params for resource-related requests. + * + * @internal + */ +export interface ResourceRequestParams extends RequestParams { + /** + * The URI of the resource. The URI can use any protocol; it is up to the server how to interpret it. + * + * @format uri + */ + uri: string; +} + +/** + * Parameters for a `resources/read` request. + * + * @category `resources/read` + */ +export interface ReadResourceRequestParams extends ResourceRequestParams, InputResponseRequestParams {} + +/** + * Sent from the client to the server, to read a specific resource URI. + * + * @example Read resource request + * {@includeCode ./examples/ReadResourceRequest/read-resource-request.json} + * + * @category `resources/read` + */ +export interface ReadResourceRequest extends JSONRPCRequest { + method: 'resources/read'; + params: ReadResourceRequestParams; +} + +/** + * The result returned by the server for a {@link ReadResourceRequest | resources/read} request. + * + * @example File resource contents + * {@includeCode ./examples/ReadResourceResult/file-resource-contents.json} + * + * @category `resources/read` + */ +export interface ReadResourceResult extends CacheableResult { + contents: (TextResourceContents | BlobResourceContents)[]; +} + +/** + * A successful response from the server for a {@link ReadResourceRequest | resources/read} request. + * + * @example Read resource result response + * {@includeCode ./examples/ReadResourceResultResponse/read-resource-result-response.json} + * + * @example Read resource result response with TTL + * {@includeCode ./examples/ReadResourceResultResponse/read-resource-result-response-with-ttl.json} + * + * @category `resources/read` + */ +export interface ReadResourceResultResponse extends JSONRPCResultResponse { + result: ReadResourceResult | InputRequiredResult; +} + +/** + * An optional notification from the server to the client, informing it that the list of resources it can read from has changed. This may be issued by servers without any previous subscription from the client. + * + * @example Resources list changed + * {@includeCode ./examples/ResourceListChangedNotification/resources-list-changed.json} + * + * @category `notifications/resources/list_changed` + */ +export interface ResourceListChangedNotification extends JSONRPCNotification { + method: 'notifications/resources/list_changed'; + params?: NotificationParams; +} + +/** + * The set of notification types a client may opt in to on a + * {@link SubscriptionsListenRequest | subscriptions/listen} request. + * + * Each notification type is **opt-in**; the server **MUST NOT** send + * notification types the client has not explicitly requested here. + * + * @category `subscriptions/listen` + */ +export interface SubscriptionFilter { + /** + * If true, receive {@link ToolListChangedNotification | notifications/tools/list_changed}. + */ + toolsListChanged?: boolean; + /** + * If true, receive {@link PromptListChangedNotification | notifications/prompts/list_changed}. + */ + promptsListChanged?: boolean; + /** + * If true, receive {@link ResourceListChangedNotification | notifications/resources/list_changed}. + */ + resourcesListChanged?: boolean; + /** + * Subscribe to {@link ResourceUpdatedNotification | notifications/resources/updated} for these resource URIs. + * Replaces the former `resources/subscribe` RPC. + */ + resourceSubscriptions?: string[]; +} + +/** + * Parameters for a {@link SubscriptionsListenRequest | subscriptions/listen} request. + * + * @category `subscriptions/listen` + */ +export interface SubscriptionsListenRequestParams extends RequestParams { + /** + * The notifications the client opts in to on this stream. The server + * **MUST NOT** send notification types the client has not explicitly + * requested. + */ + notifications: SubscriptionFilter; +} + +/** + * Sent from the client to open a long-lived channel for receiving notifications + * outside the context of a specific request. Replaces the previous HTTP GET + * endpoint and ensures consistent behavior between HTTP and STDIO. + * + * @example Listen for tools and resource list changes + * {@includeCode ./examples/SubscriptionsListenRequest/listen-for-list-changes.json} + * + * @category `subscriptions/listen` + */ +export interface SubscriptionsListenRequest extends JSONRPCRequest { + method: 'subscriptions/listen'; + params: SubscriptionsListenRequestParams; +} + +/** + * Parameters for a {@link SubscriptionsAcknowledgedNotification | notifications/subscriptions/acknowledged} notification. + * + * @category `notifications/subscriptions/acknowledged` + */ +export interface SubscriptionsAcknowledgedNotificationParams extends NotificationParams { + /** + * The subset of requested notification types the server agreed to honor. + * Only includes notification types the server actually supports; if the + * client requested an unsupported type (e.g., `promptsListChanged` when + * the server has no prompts), it is omitted from this set. + */ + notifications: SubscriptionFilter; +} + +/** + * Sent by the server as the first message on a + * {@link SubscriptionsListenRequest | subscriptions/listen} stream to acknowledge + * that the subscription has been established and to report which notification + * types it agreed to honor. + * + * @example Listen acknowledged + * {@includeCode ./examples/SubscriptionsAcknowledgedNotification/listen-acknowledged.json} + * + * @category `notifications/subscriptions/acknowledged` + */ +export interface SubscriptionsAcknowledgedNotification extends JSONRPCNotification { + method: 'notifications/subscriptions/acknowledged'; + params: SubscriptionsAcknowledgedNotificationParams; +} + +/** + * Parameters for a `notifications/resources/updated` notification. + * + * @example File resource updated + * {@includeCode ./examples/ResourceUpdatedNotificationParams/file-resource-updated.json} + * + * @category `notifications/resources/updated` + */ +export interface ResourceUpdatedNotificationParams extends NotificationParams { + /** + * The URI of the resource that has been updated. This might be a sub-resource of the one that the client actually subscribed to. + * + * @format uri + */ + uri: string; +} + +/** + * A notification from the server to the client, informing it that a resource has changed and may need to be read again. This is only sent for resources the client opted in to via the `resourceSubscriptions` field of a {@link SubscriptionsListenRequest | subscriptions/listen} request. + * + * @example File resource updated notification + * {@includeCode ./examples/ResourceUpdatedNotification/file-resource-updated-notification.json} + * + * @category `notifications/resources/updated` + */ +export interface ResourceUpdatedNotification extends JSONRPCNotification { + method: 'notifications/resources/updated'; + params: ResourceUpdatedNotificationParams; +} + +/** + * A known resource that the server is capable of reading. + * + * @example File resource with annotations + * {@includeCode ./examples/Resource/file-resource-with-annotations.json} + * + * @category `resources/list` + */ +export interface Resource extends BaseMetadata, Icons { + /** + * The URI of this resource. + * + * @format uri + */ + uri: string; + + /** + * A description of what this resource represents. + * + * This can be used by clients to improve the LLM's understanding of available resources. It can be thought of like a "hint" to the model. + */ + description?: string; + + /** + * The MIME type of this resource, if known. + */ + mimeType?: string; + + /** + * Optional annotations for the client. + */ + annotations?: Annotations; + + /** + * The size of the raw resource content, in bytes (i.e., before base64 encoding or any tokenization), if known. + * + * This can be used by Hosts to display file sizes and estimate context window usage. + */ + size?: number; + + _meta?: MetaObject; +} + +/** + * A template description for resources available on the server. + * + * @category `resources/templates/list` + */ +export interface ResourceTemplate extends BaseMetadata, Icons { + /** + * A URI template (according to RFC 6570) that can be used to construct resource URIs. + * + * @format uri-template + */ + uriTemplate: string; + + /** + * A description of what this template is for. + * + * This can be used by clients to improve the LLM's understanding of available resources. It can be thought of like a "hint" to the model. + */ + description?: string; + + /** + * The MIME type for all resources that match this template. This should only be included if all resources matching this template have the same type. + */ + mimeType?: string; + + /** + * Optional annotations for the client. + */ + annotations?: Annotations; + + _meta?: MetaObject; +} + +/** + * The contents of a specific resource or sub-resource. + * + * @internal + */ +export interface ResourceContents { + /** + * The URI of this resource. + * + * @format uri + */ + uri: string; + /** + * The MIME type of this resource, if known. + */ + mimeType?: string; + + _meta?: MetaObject; +} + +/** + * @example Text file contents + * {@includeCode ./examples/TextResourceContents/text-file-contents.json} + * + * @category Content + */ +export interface TextResourceContents extends ResourceContents { + /** + * The text of the item. This must only be set if the item can actually be represented as text (not binary data). + */ + text: string; +} + +/** + * @example Image file contents + * {@includeCode ./examples/BlobResourceContents/image-file-contents.json} + * + * @category Content + */ +export interface BlobResourceContents extends ResourceContents { + /** + * A base64-encoded string representing the binary data of the item. + * + * @format byte + */ + blob: string; +} + +/* Prompts */ +/** + * Sent from the client to request a list of prompts and prompt templates the server has. + * + * @example List prompts request + * {@includeCode ./examples/ListPromptsRequest/list-prompts-request.json} + * + * @category `prompts/list` + */ +export interface ListPromptsRequest extends PaginatedRequest { + method: 'prompts/list'; +} + +/** + * The result returned by the server for a {@link ListPromptsRequest | prompts/list} request. + * + * @example Prompts list with cursor and TTL + * {@includeCode ./examples/ListPromptsResult/prompts-list-with-cursor-and-ttl.json} + * + * @category `prompts/list` + */ +export interface ListPromptsResult extends PaginatedResult, CacheableResult { + prompts: Prompt[]; +} + +/** + * A successful response from the server for a {@link ListPromptsRequest | prompts/list} request. + * + * @example List prompts result response + * {@includeCode ./examples/ListPromptsResultResponse/list-prompts-result-response.json} + * + * @category `prompts/list` + */ +export interface ListPromptsResultResponse extends JSONRPCResultResponse { + result: ListPromptsResult; +} + +/** + * Parameters for a `prompts/get` request. + * + * @example Get code review prompt + * {@includeCode ./examples/GetPromptRequestParams/get-code-review-prompt.json} + * + * @category `prompts/get` + */ +export interface GetPromptRequestParams extends InputResponseRequestParams { + /** + * The name of the prompt or prompt template. + */ + name: string; + /** + * Arguments to use for templating the prompt. + */ + arguments?: { [key: string]: string }; +} + +/** + * Used by the client to get a prompt provided by the server. + * + * @example Get prompt request + * {@includeCode ./examples/GetPromptRequest/get-prompt-request.json} + * + * @category `prompts/get` + */ +export interface GetPromptRequest extends JSONRPCRequest { + method: 'prompts/get'; + params: GetPromptRequestParams; +} + +/** + * The result returned by the server for a {@link GetPromptRequest | prompts/get} request. + * + * @example Code review prompt + * {@includeCode ./examples/GetPromptResult/code-review-prompt.json} + * + * @category `prompts/get` + */ +export interface GetPromptResult extends Result { + /** + * An optional description for the prompt. + */ + description?: string; + messages: PromptMessage[]; +} + +/** + * A successful response from the server for a {@link GetPromptRequest | prompts/get} request. + * + * @example Get prompt result response + * {@includeCode ./examples/GetPromptResultResponse/get-prompt-result-response.json} + * + * @category `prompts/get` + */ +export interface GetPromptResultResponse extends JSONRPCResultResponse { + result: GetPromptResult | InputRequiredResult; +} + +/** + * A prompt or prompt template that the server offers. + * + * @category `prompts/list` + */ +export interface Prompt extends BaseMetadata, Icons { + /** + * An optional description of what this prompt provides + */ + description?: string; + + /** + * A list of arguments to use for templating the prompt. + */ + arguments?: PromptArgument[]; + + _meta?: MetaObject; +} + +/** + * Describes an argument that a prompt can accept. + * + * @category `prompts/list` + */ +export interface PromptArgument extends BaseMetadata { + /** + * A human-readable description of the argument. + */ + description?: string; + /** + * Whether this argument must be provided. + */ + required?: boolean; +} + +/** + * The sender or recipient of messages and data in a conversation. + * + * @category Common Types + */ +export type Role = 'user' | 'assistant'; + +/** + * Describes a message returned as part of a prompt. + * + * This is similar to {@link SamplingMessage}, but also supports the embedding of + * resources from the MCP server. + * + * @category `prompts/get` + */ +export interface PromptMessage { + role: Role; + content: ContentBlock; +} + +/** + * A resource that the server is capable of reading, included in a prompt or tool call result. + * + * Note: resource links returned by tools are not guaranteed to appear in the results of {@link ListResourcesRequest | resources/list} requests. + * + * @example File resource link + * {@includeCode ./examples/ResourceLink/file-resource-link.json} + * + * @category Content + */ +export interface ResourceLink extends Resource { + type: 'resource_link'; +} + +/** + * The contents of a resource, embedded into a prompt or tool call result. + * + * It is up to the client how best to render embedded resources for the benefit + * of the LLM and/or the user. + * + * @example Embedded file resource with annotations + * {@includeCode ./examples/EmbeddedResource/embedded-file-resource-with-annotations.json} + * + * @category Content + */ +export interface EmbeddedResource { + type: 'resource'; + resource: TextResourceContents | BlobResourceContents; + + /** + * Optional annotations for the client. + */ + annotations?: Annotations; + + _meta?: MetaObject; +} +/** + * An optional notification from the server to the client, informing it that the list of prompts it offers has changed. This may be issued by servers without any previous subscription from the client. + * + * @example Prompts list changed + * {@includeCode ./examples/PromptListChangedNotification/prompts-list-changed.json} + * + * @category `notifications/prompts/list_changed` + */ +export interface PromptListChangedNotification extends JSONRPCNotification { + method: 'notifications/prompts/list_changed'; + params?: NotificationParams; +} + +/* Tools */ +/** + * Sent from the client to request a list of tools the server has. + * + * @example List tools request + * {@includeCode ./examples/ListToolsRequest/list-tools-request.json} + * + * @category `tools/list` + */ +export interface ListToolsRequest extends PaginatedRequest { + method: 'tools/list'; +} + +/** + * The result returned by the server for a {@link ListToolsRequest | tools/list} request. + * + * @example Tools list with cursor and TTL + * {@includeCode ./examples/ListToolsResult/tools-list-with-cursor-and-ttl.json} + * + * @category `tools/list` + */ +export interface ListToolsResult extends PaginatedResult, CacheableResult { + tools: Tool[]; +} + +/** + * A successful response from the server for a {@link ListToolsRequest | tools/list} request. + * + * @example List tools result response + * {@includeCode ./examples/ListToolsResultResponse/list-tools-result-response.json} + * + * @category `tools/list` + */ +export interface ListToolsResultResponse extends JSONRPCResultResponse { + result: ListToolsResult; +} + +/** + * The result returned by the server for a {@link CallToolRequest | tools/call} request. + * + * @example Result with unstructured text + * {@includeCode ./examples/CallToolResult/result-with-unstructured-text.json} + * + * @example Result with structured content + * {@includeCode ./examples/CallToolResult/result-with-structured-content.json} + * + * @example Invalid tool input error + * {@includeCode ./examples/CallToolResult/invalid-tool-input-error.json} + * + * @category `tools/call` + */ +export interface CallToolResult extends Result { + /** + * A list of content objects that represent the unstructured result of the tool call. + */ + content: ContentBlock[]; + + /** + * An optional JSON value that represents the structured result of the tool call. + * + * This can be any JSON value (object, array, string, number, boolean, or null) + * that conforms to the tool's outputSchema if one is defined. + */ + structuredContent?: unknown; + + /** + * Whether the tool call ended in an error. + * + * If not set, this is assumed to be false (the call was successful). + * + * Any errors that originate from the tool SHOULD be reported inside the result + * object, with `isError` set to true, _not_ as an MCP protocol-level error + * response. Otherwise, the LLM would not be able to see that an error occurred + * and self-correct. + * + * However, any errors in _finding_ the tool, an error indicating that the + * server does not support tool calls, or any other exceptional conditions, + * should be reported as an MCP error response. + */ + isError?: boolean; +} + +/** + * A successful response from the server for a {@link CallToolRequest | tools/call} request. + * + * @example Call tool result response + * {@includeCode ./examples/CallToolResultResponse/call-tool-result-response.json} + * + * @category `tools/call` + */ +export interface CallToolResultResponse extends JSONRPCResultResponse { + result: CallToolResult | InputRequiredResult; +} + +/** + * Parameters for a `tools/call` request. + * + * @example `get_weather` tool call params + * {@includeCode ./examples/CallToolRequestParams/get-weather-tool-call-params.json} + * + * @example Tool call params with progress token + * {@includeCode ./examples/CallToolRequestParams/tool-call-params-with-progress-token.json} + * + * @category `tools/call` + */ +export interface CallToolRequestParams extends InputResponseRequestParams { + /** + * The name of the tool. + */ + name: string; + /** + * Arguments to use for the tool call. + */ + arguments?: { [key: string]: unknown }; +} + +/** + * Used by the client to invoke a tool provided by the server. + * + * @example Call tool request + * {@includeCode ./examples/CallToolRequest/call-tool-request.json} + * + * @category `tools/call` + */ +export interface CallToolRequest extends JSONRPCRequest { + method: 'tools/call'; + params: CallToolRequestParams; +} + +/** + * An optional notification from the server to the client, informing it that the list of tools it offers has changed. This may be issued by servers without any previous subscription from the client. + * + * @example Tools list changed + * {@includeCode ./examples/ToolListChangedNotification/tools-list-changed.json} + * + * @category `notifications/tools/list_changed` + */ +export interface ToolListChangedNotification extends JSONRPCNotification { + method: 'notifications/tools/list_changed'; + params?: NotificationParams; +} + +/** + * Additional properties describing a {@link Tool} to clients. + * + * NOTE: all properties in `ToolAnnotations` are **hints**. + * They are not guaranteed to provide a faithful description of + * tool behavior (including descriptive properties like `title`). + * + * Clients should never make tool use decisions based on `ToolAnnotations` + * received from untrusted servers. + * + * @category `tools/list` + */ +export interface ToolAnnotations { + /** + * A human-readable title for the tool. + */ + title?: string; + + /** + * If true, the tool does not modify its environment. + * + * Default: false + */ + readOnlyHint?: boolean; + + /** + * If true, the tool may perform destructive updates to its environment. + * If false, the tool performs only additive updates. + * + * (This property is meaningful only when `readOnlyHint == false`) + * + * Default: true + */ + destructiveHint?: boolean; + + /** + * If true, calling the tool repeatedly with the same arguments + * will have no additional effect on its environment. + * + * (This property is meaningful only when `readOnlyHint == false`) + * + * Default: false + */ + idempotentHint?: boolean; + + /** + * If true, this tool may interact with an "open world" of external + * entities. If false, the tool's domain of interaction is closed. + * For example, the world of a web search tool is open, whereas that + * of a memory tool is not. + * + * Default: true + */ + openWorldHint?: boolean; +} + +/** + * Definition for a tool the client can call. + * + * @example With default 2020-12 input schema + * {@includeCode ./examples/Tool/with-default-2020-12-input-schema.json} + * + * @example With explicit draft-07 input schema + * {@includeCode ./examples/Tool/with-explicit-draft-07-input-schema.json} + * + * @example With no parameters + * {@includeCode ./examples/Tool/with-no-parameters.json} + * + * @example With output schema for structured content + * {@includeCode ./examples/Tool/with-output-schema-for-structured-content.json} + * + * @category `tools/list` + */ +export interface Tool extends BaseMetadata, Icons { + /** + * A human-readable description of the tool. + * + * This can be used by clients to improve the LLM's understanding of available tools. It can be thought of like a "hint" to the model. + */ + description?: string; + + /** + * A JSON Schema object defining the expected parameters for the tool. + * + * Tool arguments are always JSON objects, so `type: "object"` is required at the root. + * Beyond that, any JSON Schema 2020-12 keyword may appear alongside `type` — including + * composition keywords (`oneOf`, `anyOf`, `allOf`, `not`), conditional keywords + * (`if`/`then`/`else`), reference keywords (`$ref`, `$defs`, `$anchor`), and any other + * standard validation or annotation keywords. + * + * Defaults to JSON Schema 2020-12 when no explicit `$schema` is provided. + */ + inputSchema: { $schema?: string; type: 'object'; [key: string]: unknown }; + + /** + * An optional JSON Schema object defining the structure of the tool's output returned in + * the structuredContent field of a {@link CallToolResult}. This can be any valid JSON Schema 2020-12. + * + * Defaults to JSON Schema 2020-12 when no explicit `$schema` is provided. + */ + outputSchema?: { $schema?: string; [key: string]: unknown }; + + /** + * Optional additional tool information. + * + * Display name precedence order is: `title`, `annotations.title`, then `name`. + */ + annotations?: ToolAnnotations; + + _meta?: MetaObject; +} + +/* Logging */ + +/** + * Parameters for a `notifications/message` notification. + * + * @deprecated Deprecated as of protocol version 2026-07-28 (SEP-2577). + * Remains in the specification for at least twelve months; see the + * deprecated features registry. + * + * @example Log database connection failed + * {@includeCode ./examples/LoggingMessageNotificationParams/log-database-connection-failed.json} + * + * @category `notifications/message` + */ +export interface LoggingMessageNotificationParams extends NotificationParams { + /** + * The severity of this log message. + */ + level: LoggingLevel; + /** + * An optional name of the logger issuing this message. + */ + logger?: string; + /** + * The data to be logged, such as a string message or an object. Any JSON serializable type is allowed here. + */ + data: unknown; +} + +/** + * JSONRPCNotification of a log message passed from server to client. The client opts in by setting `"io.modelcontextprotocol/logLevel"` in a request's `_meta`. + * + * @deprecated Deprecated as of protocol version 2026-07-28 (SEP-2577). + * Remains in the specification for at least twelve months; see the + * deprecated features registry. + * + * @example Log database connection failed + * {@includeCode ./examples/LoggingMessageNotification/log-database-connection-failed.json} + * + * @category `notifications/message` + */ +export interface LoggingMessageNotification extends JSONRPCNotification { + method: 'notifications/message'; + params: LoggingMessageNotificationParams; +} + +/** + * The severity of a log message. + * + * These map to syslog message severities, as specified in RFC-5424: + * https://datatracker.ietf.org/doc/html/rfc5424#section-6.2.1 + * + * @deprecated Deprecated as of protocol version 2026-07-28 (SEP-2577). + * Remains in the specification for at least twelve months; see the + * deprecated features registry. + * + * @category Common Types + */ +export type LoggingLevel = 'debug' | 'info' | 'notice' | 'warning' | 'error' | 'critical' | 'alert' | 'emergency'; + +/* Sampling */ +/** + * Parameters for a `sampling/createMessage` request. + * + * @example Basic request + * {@includeCode ./examples/CreateMessageRequestParams/basic-request.json} + * + * @example Request with tools + * {@includeCode ./examples/CreateMessageRequestParams/request-with-tools.json} + * + * @example Follow-up request with tool results + * {@includeCode ./examples/CreateMessageRequestParams/follow-up-with-tool-results.json} + * + * @deprecated Deprecated as of protocol version 2026-07-28 (SEP-2577). + * Remains in the specification for at least twelve months; see the + * deprecated features registry. + * + * @category `sampling/createMessage` + */ +export interface CreateMessageRequestParams { + messages: SamplingMessage[]; + /** + * The server's preferences for which model to select. The client MAY ignore these preferences. + */ + modelPreferences?: ModelPreferences; + /** + * An optional system prompt the server wants to use for sampling. The client MAY modify or omit this prompt. + */ + systemPrompt?: string; + /** + * A request to include context from one or more MCP servers (including the caller), to be attached to the prompt. + * The client MAY ignore this request. + * + * Default is `"none"`. The values `"thisServer"` and `"allServers"` are deprecated (SEP-2596): servers SHOULD + * omit this field or use `"none"`, and SHOULD only use the deprecated values if the client declares + * {@link ClientCapabilities.sampling.context}. + * + * @deprecated The `"thisServer"` and `"allServers"` values are deprecated as of protocol version 2025-11-25 + * (SEP-2596) and will be removed no later than the Sampling feature itself (SEP-2577). Omit this field or use `"none"`. + */ + includeContext?: 'none' | 'thisServer' | 'allServers'; + /** + * @TJS-type number + */ + temperature?: number; + /** + * The requested maximum number of tokens to sample (to prevent runaway completions). + * + * The client MAY choose to sample fewer tokens than the requested maximum. + */ + maxTokens: number; + stopSequences?: string[]; + /** + * Optional metadata to pass through to the LLM provider. The format of this metadata is provider-specific. + */ + metadata?: JSONObject; + /** + * Tools that the model may use during generation. + * The client MUST return an error if this field is provided but {@link ClientCapabilities.sampling.tools} is not declared. + */ + tools?: Tool[]; + /** + * Controls how the model uses tools. + * The client MUST return an error if this field is provided but {@link ClientCapabilities.sampling.tools} is not declared. + * Default is `{ mode: "auto" }`. + */ + toolChoice?: ToolChoice; +} + +/** + * Controls tool selection behavior for sampling requests. + * + * @deprecated Deprecated as of protocol version 2026-07-28 (SEP-2577). + * Remains in the specification for at least twelve months; see the + * deprecated features registry. + * + * @category `sampling/createMessage` + */ +export interface ToolChoice { + /** + * Controls the tool use ability of the model: + * - `"auto"`: Model decides whether to use tools (default) + * - `"required"`: Model MUST use at least one tool before completing + * - `"none"`: Model MUST NOT use any tools + */ + mode?: 'auto' | 'required' | 'none'; +} + +/** + * A request from the server to sample an LLM via the client. The client has full discretion over which model to select. The client should also inform the user before beginning sampling, to allow them to inspect the request (human in the loop) and decide whether to approve it. + * + * @example Sampling request + * {@includeCode ./examples/CreateMessageRequest/sampling-request.json} + * + * @deprecated Deprecated as of protocol version 2026-07-28 (SEP-2577). + * Remains in the specification for at least twelve months; see the + * deprecated features registry. + * + * @category `sampling/createMessage` + */ +export interface CreateMessageRequest { + method: 'sampling/createMessage'; + params: CreateMessageRequestParams; +} + +/** + * The result returned by the client for a {@link CreateMessageRequest | sampling/createMessage} request. + * The client should inform the user before returning the sampled message, to allow them + * to inspect the response (human in the loop) and decide whether to allow the server to see it. + * + * @example Text response + * {@includeCode ./examples/CreateMessageResult/text-response.json} + * + * @example Tool use response + * {@includeCode ./examples/CreateMessageResult/tool-use-response.json} + * + * @example Final response after tool use + * {@includeCode ./examples/CreateMessageResult/final-response.json} + * + * @deprecated Deprecated as of protocol version 2026-07-28 (SEP-2577). + * Remains in the specification for at least twelve months; see the + * deprecated features registry. + * + * @category `sampling/createMessage` + */ +export interface CreateMessageResult extends SamplingMessage { + /** + * The name of the model that generated the message. + */ + model: string; + + /** + * The reason why sampling stopped, if known. + * + * Standard values: + * - `"endTurn"`: Natural end of the assistant's turn + * - `"stopSequence"`: A stop sequence was encountered + * - `"maxTokens"`: Maximum token limit was reached + * - `"toolUse"`: The model wants to use one or more tools + * + * This field is an open string to allow for provider-specific stop reasons. + */ + stopReason?: 'endTurn' | 'stopSequence' | 'maxTokens' | 'toolUse' | string; +} + +/** + * Describes a message issued to or received from an LLM API. + * + * @example Single content block + * {@includeCode ./examples/SamplingMessage/single-content-block.json} + * + * @example Multiple content blocks + * {@includeCode ./examples/SamplingMessage/multiple-content-blocks.json} + * + * @deprecated Deprecated as of protocol version 2026-07-28 (SEP-2577). + * Remains in the specification for at least twelve months; see the + * deprecated features registry. + * + * @category `sampling/createMessage` + */ +export interface SamplingMessage { + role: Role; + content: SamplingMessageContentBlock | SamplingMessageContentBlock[]; + _meta?: MetaObject; +} + +/** + * @deprecated Deprecated as of protocol version 2026-07-28 (SEP-2577). + * Remains in the specification for at least twelve months; see the + * deprecated features registry. + * + * @category `sampling/createMessage` + */ +export type SamplingMessageContentBlock = TextContent | ImageContent | AudioContent | ToolUseContent | ToolResultContent; + +/** + * Optional annotations for the client. The client can use annotations to inform how objects are used or displayed + * + * @category Common Types + */ +export interface Annotations { + /** + * Describes who the intended audience of this object or data is. + * + * It can include multiple entries to indicate content useful for multiple audiences (e.g., `["user", "assistant"]`). + */ + audience?: Role[]; + + /** + * Describes how important this data is for operating the server. + * + * A value of 1 means "most important," and indicates that the data is + * effectively required, while 0 means "least important," and indicates that + * the data is entirely optional. + * + * @TJS-type number + * @minimum 0 + * @maximum 1 + */ + priority?: number; + + /** + * The moment the resource was last modified, as an ISO 8601 formatted string. + * + * Should be an ISO 8601 formatted string (e.g., "2025-01-12T15:00:58Z"). + * + * Examples: last activity timestamp in an open file, timestamp when the resource + * was attached, etc. + */ + lastModified?: string; +} + +/** + * @category Content + */ +export type ContentBlock = TextContent | ImageContent | AudioContent | ResourceLink | EmbeddedResource; + +/** + * Text provided to or from an LLM. + * + * @example Text content + * {@includeCode ./examples/TextContent/text-content.json} + * + * @category Content + */ +export interface TextContent { + type: 'text'; + + /** + * The text content of the message. + */ + text: string; + + /** + * Optional annotations for the client. + */ + annotations?: Annotations; + + _meta?: MetaObject; +} + +/** + * An image provided to or from an LLM. + * + * @example `image/png` content with annotations + * {@includeCode ./examples/ImageContent/image-png-content-with-annotations.json} + * + * @category Content + */ +export interface ImageContent { + type: 'image'; + + /** + * The base64-encoded image data. + * + * @format byte + */ + data: string; + + /** + * The MIME type of the image. Different providers may support different image types. + */ + mimeType: string; + + /** + * Optional annotations for the client. + */ + annotations?: Annotations; + + _meta?: MetaObject; +} + +/** + * Audio provided to or from an LLM. + * + * @example `audio/wav` content + * {@includeCode ./examples/AudioContent/audio-wav-content.json} + * + * @category Content + */ +export interface AudioContent { + type: 'audio'; + + /** + * The base64-encoded audio data. + * + * @format byte + */ + data: string; + + /** + * The MIME type of the audio. Different providers may support different audio types. + */ + mimeType: string; + + /** + * Optional annotations for the client. + */ + annotations?: Annotations; + + _meta?: MetaObject; +} + +/** + * A request from the assistant to call a tool. + * + * @example `get_weather` tool use + * {@includeCode ./examples/ToolUseContent/get-weather-tool-use.json} + * + * @deprecated Deprecated as of protocol version 2026-07-28 (SEP-2577). + * Remains in the specification for at least twelve months; see the + * deprecated features registry. + * + * @category `sampling/createMessage` + */ +export interface ToolUseContent { + type: 'tool_use'; + + /** + * A unique identifier for this tool use. + * + * This ID is used to match tool results to their corresponding tool uses. + */ + id: string; + + /** + * The name of the tool to call. + */ + name: string; + + /** + * The arguments to pass to the tool, conforming to the tool's input schema. + */ + input: { [key: string]: unknown }; + + /** + * Optional metadata about the tool use. Clients SHOULD preserve this field when + * including tool uses in subsequent sampling requests to enable caching optimizations. + */ + _meta?: MetaObject; +} + +/** + * The result of a tool use, provided by the user back to the assistant. + * + * @example `get_weather` tool result + * {@includeCode ./examples/ToolResultContent/get-weather-tool-result.json} + * + * @deprecated Deprecated as of protocol version 2026-07-28 (SEP-2577). + * Remains in the specification for at least twelve months; see the + * deprecated features registry. + * + * @category `sampling/createMessage` + */ +export interface ToolResultContent { + type: 'tool_result'; + + /** + * The ID of the tool use this result corresponds to. + * + * This MUST match the ID from a previous {@link ToolUseContent}. + */ + toolUseId: string; + + /** + * The unstructured result content of the tool use. + * + * This has the same format as {@link CallToolResult.content} and can include text, images, + * audio, resource links, and embedded resources. + */ + content: ContentBlock[]; + + /** + * An optional structured result value. + * + * This can be any JSON value (object, array, string, number, boolean, or null). + * If the tool defined an {@link Tool.outputSchema}, this SHOULD conform to that schema. + */ + structuredContent?: unknown; + + /** + * Whether the tool use resulted in an error. + * + * If true, the content typically describes the error that occurred. + * Default: false + */ + isError?: boolean; + + /** + * Optional metadata about the tool result. Clients SHOULD preserve this field when + * including tool results in subsequent sampling requests to enable caching optimizations. + */ + _meta?: MetaObject; +} + +/** + * The server's preferences for model selection, requested of the client during sampling. + * + * Because LLMs can vary along multiple dimensions, choosing the "best" model is + * rarely straightforward. Different models excel in different areas—some are + * faster but less capable, others are more capable but more expensive, and so + * on. This interface allows servers to express their priorities across multiple + * dimensions to help clients make an appropriate selection for their use case. + * + * These preferences are always advisory. The client MAY ignore them. It is also + * up to the client to decide how to interpret these preferences and how to + * balance them against other considerations. + * + * @example With hints and priorities + * {@includeCode ./examples/ModelPreferences/with-hints-and-priorities.json} + * + * @deprecated Deprecated as of protocol version 2026-07-28 (SEP-2577). + * Remains in the specification for at least twelve months; see the + * deprecated features registry. + * + * @category `sampling/createMessage` + */ +export interface ModelPreferences { + /** + * Optional hints to use for model selection. + * + * If multiple hints are specified, the client MUST evaluate them in order + * (such that the first match is taken). + * + * The client SHOULD prioritize these hints over the numeric priorities, but + * MAY still use the priorities to select from ambiguous matches. + */ + hints?: ModelHint[]; + + /** + * How much to prioritize cost when selecting a model. A value of 0 means cost + * is not important, while a value of 1 means cost is the most important + * factor. + * + * @TJS-type number + * @minimum 0 + * @maximum 1 + */ + costPriority?: number; + + /** + * How much to prioritize sampling speed (latency) when selecting a model. A + * value of 0 means speed is not important, while a value of 1 means speed is + * the most important factor. + * + * @TJS-type number + * @minimum 0 + * @maximum 1 + */ + speedPriority?: number; + + /** + * How much to prioritize intelligence and capabilities when selecting a + * model. A value of 0 means intelligence is not important, while a value of 1 + * means intelligence is the most important factor. + * + * @TJS-type number + * @minimum 0 + * @maximum 1 + */ + intelligencePriority?: number; +} + +/** + * Hints to use for model selection. + * + * Keys not declared here are currently left unspecified by the spec and are up + * to the client to interpret. + * + * @deprecated Deprecated as of protocol version 2026-07-28 (SEP-2577). + * Remains in the specification for at least twelve months; see the + * deprecated features registry. + * + * @category `sampling/createMessage` + */ +export interface ModelHint { + /** + * A hint for a model name. + * + * The client SHOULD treat this as a substring of a model name; for example: + * - `claude-3-5-sonnet` should match `claude-3-5-sonnet-20241022` + * - `sonnet` should match `claude-3-5-sonnet-20241022`, `claude-3-sonnet-20240229`, etc. + * - `claude` should match any Claude model + * + * The client MAY also map the string to a different provider's model name or a different model family, as long as it fills a similar niche; for example: + * - `gemini-1.5-flash` could match `claude-3-haiku-20240307` + */ + name?: string; +} + +/* Autocomplete */ +/** + * Parameters for a `completion/complete` request. + * + * @category `completion/complete` + * + * @example Prompt argument completion + * {@includeCode ./examples/CompleteRequestParams/prompt-argument-completion.json} + * + * @example Prompt argument completion with context + * {@includeCode ./examples/CompleteRequestParams/prompt-argument-completion-with-context.json} + */ +export interface CompleteRequestParams extends RequestParams { + ref: PromptReference | ResourceTemplateReference; + /** + * The argument's information + */ + argument: { + /** + * The name of the argument + */ + name: string; + /** + * The value of the argument to use for completion matching. + */ + value: string; + }; + + /** + * Additional, optional context for completions + */ + context?: { + /** + * Previously-resolved variables in a URI template or prompt. + */ + arguments?: { [key: string]: string }; + }; +} + +/** + * A request from the client to the server, to ask for completion options. + * + * @example Completion request + * {@includeCode ./examples/CompleteRequest/completion-request.json} + * + * @category `completion/complete` + */ +export interface CompleteRequest extends JSONRPCRequest { + method: 'completion/complete'; + params: CompleteRequestParams; +} + +/** + * The result returned by the server for a {@link CompleteRequest | completion/complete} request. + * + * @category `completion/complete` + * + * @example Single completion value + * {@includeCode ./examples/CompleteResult/single-completion-value.json} + * + * @example Multiple completion values with more available + * {@includeCode ./examples/CompleteResult/multiple-completion-values-with-more-available.json} + */ +export interface CompleteResult extends Result { + completion: { + /** + * An array of completion values. Must not exceed 100 items. + * + * @maxItems 100 + */ + values: string[]; + /** + * The total number of completion options available. This can exceed the number of values actually sent in the response. + */ + total?: number; + /** + * Indicates whether there are additional completion options beyond those provided in the current response, even if the exact total is unknown. + */ + hasMore?: boolean; + }; +} + +/** + * A successful response from the server for a {@link CompleteRequest | completion/complete} request. + * + * @example Completion result response + * {@includeCode ./examples/CompleteResultResponse/completion-result-response.json} + * + * @category `completion/complete` + */ +export interface CompleteResultResponse extends JSONRPCResultResponse { + result: CompleteResult; +} + +/** + * A reference to a resource or resource template definition. + * + * @category `completion/complete` + */ +export interface ResourceTemplateReference { + type: 'ref/resource'; + /** + * The URI or URI template of the resource. + * + * @format uri-template + */ + uri: string; +} + +/** + * Identifies a prompt. + * + * @category `completion/complete` + */ +export interface PromptReference extends BaseMetadata { + type: 'ref/prompt'; +} + +/* Roots */ +/** + * Sent from the server to request a list of root URIs from the client. Roots allow + * servers to ask for specific directories or files to operate on. A common example + * for roots is providing a set of repositories or directories a server should operate + * on. + * + * This request is typically used when the server needs to understand the file system + * structure or access specific locations that the client has permission to read from. + * + * @example List roots request + * {@includeCode ./examples/ListRootsRequest/list-roots-request.json} + * + * @deprecated Deprecated as of protocol version 2026-07-28 (SEP-2577). + * Remains in the specification for at least twelve months; see the + * deprecated features registry. + * + * @category `roots/list` + */ +export interface ListRootsRequest { + method: 'roots/list'; + params?: RequestParams; +} + +/** + * The result returned by the client for a {@link ListRootsRequest | roots/list} request. + * This result contains an array of {@link Root} objects, each representing a root directory + * or file that the server can operate on. + * + * @example Single root directory + * {@includeCode ./examples/ListRootsResult/single-root-directory.json} + * + * @example Multiple root directories + * {@includeCode ./examples/ListRootsResult/multiple-root-directories.json} + * + * @deprecated Deprecated as of protocol version 2026-07-28 (SEP-2577). + * Remains in the specification for at least twelve months; see the + * deprecated features registry. + * + * @category `roots/list` + */ +export interface ListRootsResult { + roots: Root[]; +} + +/** + * Represents a root directory or file that the server can operate on. + * + * @example Project directory root + * {@includeCode ./examples/Root/project-directory.json} + * + * @deprecated Deprecated as of protocol version 2026-07-28 (SEP-2577). + * Remains in the specification for at least twelve months; see the + * deprecated features registry. + * + * @category `roots/list` + */ +export interface Root { + /** + * The URI identifying the root. This *must* start with `file://` for now. + * This restriction may be relaxed in future versions of the protocol to allow + * other URI schemes. + * + * @format uri + */ + uri: string; + /** + * An optional name for the root. This can be used to provide a human-readable + * identifier for the root, which may be useful for display purposes or for + * referencing the root in other parts of the application. + */ + name?: string; + + _meta?: MetaObject; +} + +/** + * The parameters for a request to elicit non-sensitive information from the user via a form in the client. + * + * @example Elicit single field + * {@includeCode ./examples/ElicitRequestFormParams/elicit-single-field.json} + * + * @example Elicit multiple fields + * {@includeCode ./examples/ElicitRequestFormParams/elicit-multiple-fields.json} + * + * @category `elicitation/create` + */ +export interface ElicitRequestFormParams { + /** + * The elicitation mode. + */ + mode?: 'form'; + + /** + * The message to present to the user describing what information is being requested. + */ + message: string; + + /** + * A restricted subset of JSON Schema. + * Only top-level properties are allowed, without nesting. + */ + requestedSchema: { + $schema?: string; + type: 'object'; + properties: { + [key: string]: PrimitiveSchemaDefinition; + }; + required?: string[]; + }; +} + +/** + * The parameters for a request to elicit information from the user via a URL in the client. + * + * @example Elicit sensitive data + * {@includeCode ./examples/ElicitRequestURLParams/elicit-sensitive-data.json} + * + * @category `elicitation/create` + */ +export interface ElicitRequestURLParams { + /** + * The elicitation mode. + */ + mode: 'url'; + + /** + * The message to present to the user explaining why the interaction is needed. + */ + message: string; + + /** + * The ID of the elicitation, which must be unique within the context of the server. + * The client MUST treat this ID as an opaque value. + */ + elicitationId: string; + + /** + * The URL that the user should navigate to. + * + * @format uri + */ + url: string; +} + +/** + * The parameters for a request to elicit additional information from the user via the client. + * + * @category `elicitation/create` + */ +export type ElicitRequestParams = ElicitRequestFormParams | ElicitRequestURLParams; + +/** + * A request from the server to elicit additional information from the user via the client. + * + * @example Elicitation request + * {@includeCode ./examples/ElicitRequest/elicitation-request.json} + * + * @category `elicitation/create` + */ +export interface ElicitRequest { + method: 'elicitation/create'; + params: ElicitRequestParams; +} + +/** + * Restricted schema definitions that only allow primitive types + * without nested objects or arrays. + * + * @category `elicitation/create` + */ +export type PrimitiveSchemaDefinition = StringSchema | NumberSchema | BooleanSchema | EnumSchema; + +/** + * @example Email input schema + * {@includeCode ./examples/StringSchema/email-input-schema.json} + * + * @category `elicitation/create` + */ +export interface StringSchema { + type: 'string'; + title?: string; + description?: string; + minLength?: number; + maxLength?: number; + format?: 'email' | 'uri' | 'date' | 'date-time'; + default?: string; +} + +/** + * @example Number input schema + * {@includeCode ./examples/NumberSchema/number-input-schema.json} + * + * @category `elicitation/create` + */ +export interface NumberSchema { + type: 'number' | 'integer'; + title?: string; + description?: string; + /** + * @TJS-type number + */ + minimum?: number; + /** + * @TJS-type number + */ + maximum?: number; + /** + * @TJS-type number + */ + default?: number; +} + +/** + * @example Boolean input schema + * {@includeCode ./examples/BooleanSchema/boolean-input-schema.json} + * + * @category `elicitation/create` + */ +export interface BooleanSchema { + type: 'boolean'; + title?: string; + description?: string; + default?: boolean; +} + +/** + * Schema for single-selection enumeration without display titles for options. + * + * @example Color select schema + * {@includeCode ./examples/UntitledSingleSelectEnumSchema/color-select-schema.json} + * + * @category `elicitation/create` + */ +export interface UntitledSingleSelectEnumSchema { + type: 'string'; + /** + * Optional title for the enum field. + */ + title?: string; + /** + * Optional description for the enum field. + */ + description?: string; + /** + * Array of enum values to choose from. + */ + enum: string[]; + /** + * Optional default value. + */ + default?: string; +} + +/** + * Schema for single-selection enumeration with display titles for each option. + * + * @example Titled color select schema + * {@includeCode ./examples/TitledSingleSelectEnumSchema/titled-color-select-schema.json} + * + * @category `elicitation/create` + */ +export interface TitledSingleSelectEnumSchema { + type: 'string'; + /** + * Optional title for the enum field. + */ + title?: string; + /** + * Optional description for the enum field. + */ + description?: string; + /** + * Array of enum options with values and display labels. + */ + oneOf: Array<{ + /** + * The enum value. + */ + const: string; + /** + * Display label for this option. + */ + title: string; + }>; + /** + * Optional default value. + */ + default?: string; +} + +/** + * @category `elicitation/create` + */ +// Combined single selection enumeration +export type SingleSelectEnumSchema = UntitledSingleSelectEnumSchema | TitledSingleSelectEnumSchema; + +/** + * Schema for multiple-selection enumeration without display titles for options. + * + * @example Color multi-select schema + * {@includeCode ./examples/UntitledMultiSelectEnumSchema/color-multi-select-schema.json} + * + * @category `elicitation/create` + */ +export interface UntitledMultiSelectEnumSchema { + type: 'array'; + /** + * Optional title for the enum field. + */ + title?: string; + /** + * Optional description for the enum field. + */ + description?: string; + /** + * Minimum number of items to select. + */ + minItems?: number; + /** + * Maximum number of items to select. + */ + maxItems?: number; + /** + * Schema for the array items. + */ + items: { + type: 'string'; + /** + * Array of enum values to choose from. + */ + enum: string[]; + }; + /** + * Optional default value. + */ + default?: string[]; +} + +/** + * Schema for multiple-selection enumeration with display titles for each option. + * + * @example Titled color multi-select schema + * {@includeCode ./examples/TitledMultiSelectEnumSchema/titled-color-multi-select-schema.json} + * + * @category `elicitation/create` + */ +export interface TitledMultiSelectEnumSchema { + type: 'array'; + /** + * Optional title for the enum field. + */ + title?: string; + /** + * Optional description for the enum field. + */ + description?: string; + /** + * Minimum number of items to select. + */ + minItems?: number; + /** + * Maximum number of items to select. + */ + maxItems?: number; + /** + * Schema for array items with enum options and display labels. + */ + items: { + /** + * Array of enum options with values and display labels. + */ + anyOf: Array<{ + /** + * The constant enum value. + */ + const: string; + /** + * Display title for this option. + */ + title: string; + }>; + }; + /** + * Optional default value. + */ + default?: string[]; +} + +/** + * @category `elicitation/create` + */ +// Combined multiple selection enumeration +export type MultiSelectEnumSchema = UntitledMultiSelectEnumSchema | TitledMultiSelectEnumSchema; + +/** + * Use {@link TitledSingleSelectEnumSchema} instead. + * This interface will be removed in a future version. + * + * @category `elicitation/create` + */ +export interface LegacyTitledEnumSchema { + type: 'string'; + title?: string; + description?: string; + enum: string[]; + /** + * (Legacy) Display names for enum values. + * Non-standard according to JSON schema 2020-12. + */ + enumNames?: string[]; + default?: string; +} + +/** + * @category `elicitation/create` + */ +// Union type for all enum schemas +export type EnumSchema = SingleSelectEnumSchema | MultiSelectEnumSchema | LegacyTitledEnumSchema; + +/** + * The result returned by the client for an {@link ElicitRequest| elicitation/create} request. + * + * @example Input single field + * {@includeCode ./examples/ElicitResult/input-single-field.json} + * + * @example Input multiple fields + * {@includeCode ./examples/ElicitResult/input-multiple-fields.json} + * + * @example Accept URL mode (no content) + * {@includeCode ./examples/ElicitResult/accept-url-mode-no-content.json} + * + * @category `elicitation/create` + */ +export interface ElicitResult { + /** + * The user action in response to the elicitation. + * - `"accept"`: User submitted the form/confirmed the action + * - `"decline"`: User explicitly declined the action + * - `"cancel"`: User dismissed without making an explicit choice + */ + action: 'accept' | 'decline' | 'cancel'; + + /** + * The submitted form data, only present when action is `"accept"` and mode was `"form"`. + * Contains values matching the requested schema. + * Omitted for out-of-band mode responses. + */ + content?: { [key: string]: string | number | boolean | string[] }; +} + +/** + * An optional notification from the server to the client, informing it of a completion of a out-of-band elicitation request. + * + * @example Elicitation complete + * {@includeCode ./examples/ElicitationCompleteNotification/elicitation-complete.json} + * + * @category `notifications/elicitation/complete` + */ +export interface ElicitationCompleteNotification extends JSONRPCNotification { + method: 'notifications/elicitation/complete'; + params: { + /** + * The ID of the elicitation that completed. + */ + elicitationId: string; + }; +} + +/* Client messages */ +/** @internal */ +export type ClientRequest = + | DiscoverRequest + | CompleteRequest + | GetPromptRequest + | ListPromptsRequest + | ListResourcesRequest + | ListResourceTemplatesRequest + | ReadResourceRequest + | SubscriptionsListenRequest + | CallToolRequest + | ListToolsRequest; + +/** @internal */ +export type ClientNotification = CancelledNotification | ProgressNotification; + +/** @internal */ +export type ClientResult = EmptyResult; + +/* Server messages */ + +/** @internal */ +export type ServerNotification = + | CancelledNotification + | ProgressNotification + | LoggingMessageNotification + | ResourceUpdatedNotification + | ResourceListChangedNotification + | ToolListChangedNotification + | PromptListChangedNotification + | ElicitationCompleteNotification + | SubscriptionsAcknowledgedNotification; + +/** @internal */ +export type ServerResult = + | EmptyResult + | DiscoverResult + | CompleteResult + | GetPromptResult + | ListPromptsResult + | ListResourceTemplatesResult + | ListResourcesResult + | ReadResourceResult + | CallToolResult + | ListToolsResult + | InputRequiredResult; diff --git a/packages/core-internal/src/types/specTypeSchema.examples.ts b/packages/core-internal/src/types/specTypeSchema.examples.ts new file mode 100644 index 0000000000..c05f65e62d --- /dev/null +++ b/packages/core-internal/src/types/specTypeSchema.examples.ts @@ -0,0 +1,40 @@ +/** + * Type-checked examples for `specTypeSchema.ts`. + * + * These examples are synced into JSDoc comments via the sync-snippets script. + * Each function's region markers define the code snippet that appears in the docs. + * + * @module + */ + +import { isSpecType, specTypeSchemas } from './specTypeSchema'; + +declare const untrusted: unknown; +declare const value: unknown; +declare const mixed: unknown[]; + +function specTypeSchemas_basicUsage() { + //#region specTypeSchemas_basicUsage + const result = specTypeSchemas.CallToolResult['~standard'].validate(untrusted); + if (result.issues === undefined) { + // result.value is CallToolResult + } + //#endregion specTypeSchemas_basicUsage + void result; +} + +function isSpecType_basicUsage() { + /* eslint-disable unicorn/no-array-callback-reference -- showcasing the guard-as-callback pattern */ + //#region isSpecType_basicUsage + if (isSpecType.ContentBlock(value)) { + // value is ContentBlock + } + + const blocks = mixed.filter(isSpecType.ContentBlock); + //#endregion isSpecType_basicUsage + /* eslint-enable unicorn/no-array-callback-reference */ + void blocks; +} + +void specTypeSchemas_basicUsage; +void isSpecType_basicUsage; diff --git a/packages/core-internal/src/types/specTypeSchema.ts b/packages/core-internal/src/types/specTypeSchema.ts new file mode 100644 index 0000000000..95aa718d5c --- /dev/null +++ b/packages/core-internal/src/types/specTypeSchema.ts @@ -0,0 +1,301 @@ +import type * as z from 'zod/v4'; + +import { + IdJagTokenExchangeResponseSchema, + OAuthClientInformationFullSchema, + OAuthClientInformationSchema, + OAuthClientMetadataSchema, + OAuthClientRegistrationErrorSchema, + OAuthErrorResponseSchema, + OAuthMetadataSchema, + OAuthProtectedResourceMetadataSchema, + OAuthTokenRevocationRequestSchema, + OAuthTokensSchema, + OpenIdProviderDiscoveryMetadataSchema, + OpenIdProviderMetadataSchema +} from '../shared/auth'; +import type { StandardSchemaV1, StandardSchemaV1Sync } from '../util/standardSchema'; +import * as schemas from './schemas'; + +/** + * Explicit allowlist of protocol Zod schemas that correspond to a public spec type in `types.ts`. + * + * This intentionally excludes internal helper schemas exported from `schemas.ts` that have no + * matching public type (e.g. `ListChangedOptionsBaseSchema`, `BaseRequestParamsSchema`, + * `NotificationsParamsSchema`, `ClientTasksCapabilitySchema`, `ServerTasksCapabilitySchema`). + * Keeping the list explicit means new public spec types must be added here deliberately, and + * internals never leak into `SpecTypeName`. + * + * `ResourceTemplateSchema` is included; its public type is exported as `ResourceTemplateType` + * (the bare name collides with the server package's `ResourceTemplate` class), so + * `SpecTypes['ResourceTemplate']` is structurally equal to `ResourceTemplateType` rather than to + * a type literally named `ResourceTemplate`. + */ +const SPEC_SCHEMA_KEYS = [ + 'AnnotationsSchema', + 'AudioContentSchema', + 'BaseMetadataSchema', + 'BlobResourceContentsSchema', + 'BooleanSchemaSchema', + 'CallToolRequestSchema', + 'CallToolRequestParamsSchema', + 'CallToolResultSchema', + 'CancelledNotificationSchema', + 'CancelledNotificationParamsSchema', + 'CancelTaskRequestSchema', + 'CancelTaskResultSchema', + 'ClientCapabilitiesSchema', + 'ClientNotificationSchema', + 'ClientRequestSchema', + 'ClientResultSchema', + 'CompatibilityCallToolResultSchema', + 'CompleteRequestSchema', + 'CompleteRequestParamsSchema', + 'CompleteResultSchema', + 'ContentBlockSchema', + 'CreateMessageRequestSchema', + 'CreateMessageRequestParamsSchema', + 'CreateMessageResultSchema', + 'CreateMessageResultWithToolsSchema', + 'CreateTaskResultSchema', + 'CursorSchema', + 'DiscoverRequestSchema', + 'DiscoverResultSchema', + 'ElicitationCompleteNotificationSchema', + 'ElicitationCompleteNotificationParamsSchema', + 'ElicitRequestSchema', + 'ElicitRequestFormParamsSchema', + 'ElicitRequestParamsSchema', + 'ElicitRequestURLParamsSchema', + 'ElicitResultSchema', + 'EmbeddedResourceSchema', + 'EmptyResultSchema', + 'EnumSchemaSchema', + 'GetPromptRequestSchema', + 'GetPromptRequestParamsSchema', + 'GetPromptResultSchema', + 'GetTaskPayloadRequestSchema', + 'GetTaskPayloadResultSchema', + 'GetTaskRequestSchema', + 'GetTaskResultSchema', + 'IconSchema', + 'IconsSchema', + 'ImageContentSchema', + 'ImplementationSchema', + 'InitializedNotificationSchema', + 'InitializeRequestSchema', + 'InitializeRequestParamsSchema', + 'InitializeResultSchema', + 'JSONArraySchema', + 'JSONObjectSchema', + 'JSONRPCErrorResponseSchema', + 'JSONRPCMessageSchema', + 'JSONRPCNotificationSchema', + 'JSONRPCRequestSchema', + 'JSONRPCResponseSchema', + 'JSONRPCResultResponseSchema', + 'JSONValueSchema', + 'LegacyTitledEnumSchemaSchema', + 'ListPromptsRequestSchema', + 'ListPromptsResultSchema', + 'ListResourcesRequestSchema', + 'ListResourcesResultSchema', + 'ListResourceTemplatesRequestSchema', + 'ListResourceTemplatesResultSchema', + 'ListRootsRequestSchema', + 'ListRootsResultSchema', + 'ListTasksRequestSchema', + 'ListTasksResultSchema', + 'ListToolsRequestSchema', + 'ListToolsResultSchema', + 'LoggingLevelSchema', + 'LoggingMessageNotificationSchema', + 'LoggingMessageNotificationParamsSchema', + 'ModelHintSchema', + 'ModelPreferencesSchema', + 'MultiSelectEnumSchemaSchema', + 'NotificationSchema', + 'NumberSchemaSchema', + 'PaginatedRequestSchema', + 'PaginatedRequestParamsSchema', + 'PaginatedResultSchema', + 'PingRequestSchema', + 'PrimitiveSchemaDefinitionSchema', + 'ProgressSchema', + 'ProgressNotificationSchema', + 'ProgressNotificationParamsSchema', + 'ProgressTokenSchema', + 'PromptSchema', + 'PromptArgumentSchema', + 'PromptListChangedNotificationSchema', + 'PromptMessageSchema', + 'PromptReferenceSchema', + 'ReadResourceRequestSchema', + 'ReadResourceRequestParamsSchema', + 'ReadResourceResultSchema', + 'RelatedTaskMetadataSchema', + 'RequestSchema', + 'RequestIdSchema', + 'RequestMetaEnvelopeSchema', + 'RequestMetaSchema', + 'ResourceSchema', + 'ResourceContentsSchema', + 'ResourceLinkSchema', + 'ResourceListChangedNotificationSchema', + 'ResourceRequestParamsSchema', + 'ResourceTemplateSchema', + 'ResourceTemplateReferenceSchema', + 'ResourceUpdatedNotificationSchema', + 'ResourceUpdatedNotificationParamsSchema', + 'ResultSchema', + 'RoleSchema', + 'RootSchema', + 'RootsListChangedNotificationSchema', + 'SamplingContentSchema', + 'SamplingMessageSchema', + 'SamplingMessageContentBlockSchema', + 'ServerCapabilitiesSchema', + 'ServerNotificationSchema', + 'ServerRequestSchema', + 'ServerResultSchema', + 'SetLevelRequestSchema', + 'SetLevelRequestParamsSchema', + 'SingleSelectEnumSchemaSchema', + 'StringSchemaSchema', + 'SubscribeRequestSchema', + 'SubscribeRequestParamsSchema', + 'TaskSchema', + 'TaskAugmentedRequestParamsSchema', + 'TaskCreationParamsSchema', + 'TaskMetadataSchema', + 'TaskStatusSchema', + 'TaskStatusNotificationSchema', + 'TaskStatusNotificationParamsSchema', + 'TextContentSchema', + 'TextResourceContentsSchema', + 'TitledMultiSelectEnumSchemaSchema', + 'TitledSingleSelectEnumSchemaSchema', + 'ToolSchema', + 'ToolAnnotationsSchema', + 'ToolChoiceSchema', + 'ToolExecutionSchema', + 'ToolListChangedNotificationSchema', + 'ToolResultContentSchema', + 'ToolUseContentSchema', + 'UnsubscribeRequestSchema', + 'UnsubscribeRequestParamsSchema', + 'UntitledMultiSelectEnumSchemaSchema', + 'UntitledSingleSelectEnumSchemaSchema' +] as const satisfies readonly (keyof typeof schemas)[]; + +const authSchemas = { + IdJagTokenExchangeResponseSchema, + OAuthClientInformationFullSchema, + OAuthClientInformationSchema, + OAuthClientMetadataSchema, + OAuthClientRegistrationErrorSchema, + OAuthErrorResponseSchema, + OAuthMetadataSchema, + OAuthProtectedResourceMetadataSchema, + OAuthTokenRevocationRequestSchema, + OAuthTokensSchema, + OpenIdProviderDiscoveryMetadataSchema, + OpenIdProviderMetadataSchema +} as const; + +type ProtocolSchemaKey = (typeof SPEC_SCHEMA_KEYS)[number]; +type AuthSchemaKey = keyof typeof authSchemas; +type SchemaKey = ProtocolSchemaKey | AuthSchemaKey; + +type SchemaFor = K extends ProtocolSchemaKey + ? (typeof schemas)[K] + : K extends AuthSchemaKey + ? (typeof authSchemas)[K] + : never; + +type StripSchemaSuffix = K extends `${infer N}Schema` ? N : never; + +/** + * Union of every named type in the SDK's protocol and OAuth schemas (e.g. `'CallToolResult'`, + * `'ContentBlock'`, `'Tool'`, `'OAuthTokens'`). Derived from the internal Zod schemas, so it stays + * in sync with the spec. + */ +export type SpecTypeName = StripSchemaSuffix; + +/** + * Maps each {@linkcode SpecTypeName} to its TypeScript type. + * + * `SpecTypes['CallToolResult']` is equivalent to importing the `CallToolResult` type directly. + */ +export type SpecTypes = { + [K in SchemaKey as StripSchemaSuffix]: SchemaFor extends z.ZodType ? z.output> : never; +}; + +/** + * Input shape for each {@linkcode SpecTypeName}. For most types this equals {@linkcode SpecTypes}, + * but a few schemas apply defaults/preprocessing, so the accepted input may be looser than the + * resulting output type. + */ +type SpecTypeInputs = { + [K in SchemaKey as StripSchemaSuffix]: SchemaFor extends z.ZodType ? z.input> : never; +}; + +type SchemaRecord = { readonly [K in SpecTypeName]: StandardSchemaV1Sync }; +type GuardRecord = { readonly [K in SpecTypeName]: (value: unknown) => value is SpecTypeInputs[K] }; + +const _specTypeSchemas: Record = {}; +const _isSpecType: Record boolean> = {}; +function register(key: string, schema: z.ZodType): void { + const name = key.slice(0, -'Schema'.length); + _specTypeSchemas[name] = schema; + _isSpecType[name] = (v: unknown) => schema.safeParse(v).success; +} +for (const key of SPEC_SCHEMA_KEYS) { + // eslint-disable-next-line import/namespace -- key is constrained to keyof typeof schemas via the satisfies clause above + register(key, schemas[key]); +} +for (const [key, schema] of Object.entries(authSchemas)) { + register(key, schema); +} + +/** + * Runtime validators for every MCP spec type, keyed by type name. + * + * Use this when you need to validate a spec-defined shape at a boundary the SDK does not own, for + * example an extension's custom-method payload that embeds a `CallToolResult`, or a value read from + * storage that should be a `Tool`. + * + * Each entry implements the Standard Schema interface, so it composes with any + * Standard-Schema-aware library. For a simple boolean check, use {@linkcode isSpecType} instead. + * + * @example + * ```ts source="./specTypeSchema.examples.ts#specTypeSchemas_basicUsage" + * const result = specTypeSchemas.CallToolResult['~standard'].validate(untrusted); + * if (result.issues === undefined) { + * // result.value is CallToolResult + * } + * ``` + */ +export const specTypeSchemas: SchemaRecord = Object.freeze(_specTypeSchemas as SchemaRecord); + +/** + * Type predicates for every MCP spec type, keyed by type name. + * + * Returns `true` if the value satisfies the schema's input type (`z.input<>`, before defaults and + * transforms are applied), and narrows to that input type. For schemas with `.default()` or + * `.preprocess()`, this may accept values that do not structurally match the named output type; + * for example `isSpecType.CallToolResult({})` is `true` because `content` has a default. Use + * `specTypeSchemas.X['~standard'].validate(value)` when you need the validated output value. + * + * Each guard is a standalone function, so it can be passed directly as a callback. + * + * @example + * ```ts source="./specTypeSchema.examples.ts#isSpecType_basicUsage" + * if (isSpecType.ContentBlock(value)) { + * // value is ContentBlock + * } + * + * const blocks = mixed.filter(isSpecType.ContentBlock); + * ``` + */ +export const isSpecType: GuardRecord = Object.freeze(_isSpecType as GuardRecord); diff --git a/packages/core-internal/src/types/types.ts b/packages/core-internal/src/types/types.ts new file mode 100644 index 0000000000..9abc68f79e --- /dev/null +++ b/packages/core-internal/src/types/types.ts @@ -0,0 +1,604 @@ +// ⚠️ PUBLIC API — every export from this file is re-exported via `export *` +// in exports/public/index.ts and becomes part of the SDK's public surface. +// Only add MCP-spec-derived types here. Internal helpers belong elsewhere. + +import type * as z from 'zod/v4'; + +import type { INTERNAL_ERROR, INVALID_PARAMS, INVALID_REQUEST, METHOD_NOT_FOUND, PARSE_ERROR } from './constants'; +import type { + AnnotationsSchema, + AudioContentSchema, + BaseMetadataSchema, + BaseRequestParamsSchema, + BlobResourceContentsSchema, + BooleanSchemaSchema, + CallToolRequestParamsSchema, + CallToolRequestSchema, + CallToolResultSchema, + CancelledNotificationParamsSchema, + CancelledNotificationSchema, + CancelTaskRequestSchema, + CancelTaskResultSchema, + ClientCapabilitiesSchema, + ClientNotificationSchema, + ClientRequestSchema, + ClientResultSchema, + CompatibilityCallToolResultSchema, + CompleteRequestParamsSchema, + CompleteRequestSchema, + CompleteResultSchema, + ContentBlockSchema, + CreateMessageRequestParamsSchema, + CreateMessageRequestSchema, + CreateMessageResultSchema, + CreateMessageResultWithToolsSchema, + CreateTaskResultSchema, + CursorSchema, + DiscoverRequestSchema, + DiscoverResultSchema, + ElicitationCompleteNotificationParamsSchema, + ElicitationCompleteNotificationSchema, + ElicitRequestFormParamsSchema, + ElicitRequestParamsSchema, + ElicitRequestSchema, + ElicitRequestURLParamsSchema, + ElicitResultSchema, + EmbeddedResourceSchema, + EmptyResultSchema, + EnumSchemaSchema, + GetPromptRequestParamsSchema, + GetPromptRequestSchema, + GetPromptResultSchema, + GetTaskPayloadRequestSchema, + GetTaskPayloadResultSchema, + GetTaskRequestSchema, + GetTaskResultSchema, + IconSchema, + IconsSchema, + ImageContentSchema, + ImplementationSchema, + InitializedNotificationSchema, + InitializeRequestParamsSchema, + InitializeRequestSchema, + InitializeResultSchema, + JSONRPCErrorResponseSchema, + JSONRPCMessageSchema, + JSONRPCNotificationSchema, + JSONRPCRequestSchema, + JSONRPCResponseSchema, + JSONRPCResultResponseSchema, + LegacyTitledEnumSchemaSchema, + ListPromptsRequestSchema, + ListPromptsResultSchema, + ListResourcesRequestSchema, + ListResourcesResultSchema, + ListResourceTemplatesRequestSchema, + ListResourceTemplatesResultSchema, + ListRootsRequestSchema, + ListRootsResultSchema, + ListTasksRequestSchema, + ListTasksResultSchema, + ListToolsRequestSchema, + ListToolsResultSchema, + LoggingLevelSchema, + LoggingMessageNotificationParamsSchema, + LoggingMessageNotificationSchema, + ModelHintSchema, + ModelPreferencesSchema, + MultiSelectEnumSchemaSchema, + NotificationSchema, + NotificationsParamsSchema, + NumberSchemaSchema, + PaginatedRequestParamsSchema, + PaginatedRequestSchema, + PaginatedResultSchema, + PingRequestSchema, + PrimitiveSchemaDefinitionSchema, + ProgressNotificationParamsSchema, + ProgressNotificationSchema, + ProgressSchema, + ProgressTokenSchema, + PromptArgumentSchema, + PromptListChangedNotificationSchema, + PromptMessageSchema, + PromptReferenceSchema, + PromptSchema, + ReadResourceRequestParamsSchema, + ReadResourceRequestSchema, + ReadResourceResultSchema, + RelatedTaskMetadataSchema, + RequestIdSchema, + RequestMetaEnvelopeSchema, + RequestMetaSchema, + RequestSchema, + ResourceContentsSchema, + ResourceLinkSchema, + ResourceListChangedNotificationSchema, + ResourceRequestParamsSchema, + ResourceSchema, + ResourceTemplateReferenceSchema, + ResourceTemplateSchema, + ResourceUpdatedNotificationParamsSchema, + ResourceUpdatedNotificationSchema, + ResultSchema, + RoleSchema, + RootSchema, + RootsListChangedNotificationSchema, + SamplingContentSchema, + SamplingMessageContentBlockSchema, + SamplingMessageSchema, + ServerCapabilitiesSchema, + ServerNotificationSchema, + ServerRequestSchema, + ServerResultSchema, + SetLevelRequestParamsSchema, + SetLevelRequestSchema, + SingleSelectEnumSchemaSchema, + StringSchemaSchema, + SubscribeRequestParamsSchema, + SubscribeRequestSchema, + TaskAugmentedRequestParamsSchema, + TaskCreationParamsSchema, + TaskMetadataSchema, + TaskSchema, + TaskStatusNotificationParamsSchema, + TaskStatusNotificationSchema, + TaskStatusSchema, + TextContentSchema, + TextResourceContentsSchema, + TitledMultiSelectEnumSchemaSchema, + TitledSingleSelectEnumSchemaSchema, + ToolAnnotationsSchema, + ToolChoiceSchema, + ToolExecutionSchema, + ToolListChangedNotificationSchema, + ToolResultContentSchema, + ToolSchema, + ToolUseContentSchema, + UnsubscribeRequestParamsSchema, + UnsubscribeRequestSchema, + UntitledMultiSelectEnumSchemaSchema, + UntitledSingleSelectEnumSchemaSchema +} from './schemas'; + +/* JSON types */ +export type JSONValue = string | number | boolean | null | JSONObject | JSONArray; +export type JSONObject = { [key: string]: JSONValue }; +export type JSONArray = JSONValue[]; + +/** + * Utility types + */ +type ExpandRecursively = T extends object ? (T extends infer O ? { [K in keyof O]: ExpandRecursively } : never) : T; + +type Primitive = string | number | boolean | bigint | null | undefined; +type Flatten = T extends Primitive + ? T + : T extends Array + ? Array> + : T extends Set + ? Set> + : T extends Map + ? Map, Flatten> + : T extends object + ? { [K in keyof T]: Flatten } + : T; + +type Infer = Flatten>; + +/* JSON-RPC types */ +export type ProgressToken = Infer; +export type Cursor = Infer; +export type Request = Infer; +export type TaskAugmentedRequestParams = Infer; +export type RequestMeta = Infer; +export type Notification = Infer; +export type Result = Infer; +export type RequestId = Infer; +export type JSONRPCRequest = Infer; +export type JSONRPCNotification = Infer; +export type JSONRPCResponse = Infer; +export type JSONRPCErrorResponse = Infer; +export type JSONRPCResultResponse = Infer; +export type JSONRPCMessage = Infer; +export type RequestParams = Infer; +export type NotificationParams = Infer; +/** + * The per-request `_meta` envelope carried by every request under protocol revision + * 2026-07-28 (protocol version, client info, client capabilities, optional log level). + */ +export type RequestMetaEnvelope = Infer; + +/* Empty result */ +export type EmptyResult = Infer; + +/* Cancellation */ +export type CancelledNotificationParams = Infer; +export type CancelledNotification = Infer; + +/* Base Metadata */ +export type Icon = Infer; +export type Icons = Infer; +export type BaseMetadata = Infer; +export type Annotations = Infer; +export type Role = Infer; + +/* Initialization */ +export type Implementation = Infer; +/** + * Capabilities a client may support. + * + * Note: the `roots` and `sampling` capabilities are deprecated as of protocol + * version 2026-07-28 (SEP-2577); they remain in the specification for at least + * twelve months. See `ClientCapabilitiesSchema`. + */ +export type ClientCapabilities = Infer; +export type InitializeRequestParams = Infer; +export type InitializeRequest = Infer; +/** + * Capabilities a server may support. + * + * Note: the `logging` capability is deprecated as of protocol version + * 2026-07-28 (SEP-2577); it remains in the specification for at least twelve + * months. See `ServerCapabilitiesSchema`. + */ +export type ServerCapabilities = Infer; +export type InitializeResult = Infer; +export type InitializedNotification = Infer; + +/* Discovery */ +export type DiscoverRequest = Infer; +export type DiscoverResult = Infer; + +/* Ping */ +export type PingRequest = Infer; + +/* Progress notifications */ +export type Progress = Infer; +export type ProgressNotificationParams = Infer; +export type ProgressNotification = Infer; + +/* Tasks */ +export type Task = Infer; +export type TaskStatus = Infer; +export type TaskCreationParams = Infer; +export type TaskMetadata = Infer; +export type RelatedTaskMetadata = Infer; +export type CreateTaskResult = Infer; +export type TaskStatusNotificationParams = Infer; +export type TaskStatusNotification = Infer; +export type GetTaskRequest = Infer; +export type GetTaskResult = Infer; +export type GetTaskPayloadRequest = Infer; +export type ListTasksRequest = Infer; +export type ListTasksResult = Infer; +export type CancelTaskRequest = Infer; +export type CancelTaskResult = Infer; +export type GetTaskPayloadResult = Infer; + +/* Pagination */ +export type PaginatedRequestParams = Infer; +export type PaginatedRequest = Infer; +export type PaginatedResult = Infer; + +/* Resources */ +export type ResourceContents = Infer; +export type TextResourceContents = Infer; +export type BlobResourceContents = Infer; +export type Resource = Infer; +// TODO: Overlaps with exported `ResourceTemplate` class from `server`. +export type ResourceTemplateType = Infer; +export type ListResourcesRequest = Infer; +export type ListResourcesResult = Infer; +export type ListResourceTemplatesRequest = Infer; +export type ListResourceTemplatesResult = Infer; +export type ResourceRequestParams = Infer; +export type ReadResourceRequestParams = Infer; +export type ReadResourceRequest = Infer; +export type ReadResourceResult = Infer; +export type ResourceListChangedNotification = Infer; +export type SubscribeRequestParams = Infer; +export type SubscribeRequest = Infer; +export type UnsubscribeRequestParams = Infer; +export type UnsubscribeRequest = Infer; +export type ResourceUpdatedNotificationParams = Infer; +export type ResourceUpdatedNotification = Infer; + +/* Prompts */ +export type PromptArgument = Infer; +export type Prompt = Infer; +export type ListPromptsRequest = Infer; +export type ListPromptsResult = Infer; +export type GetPromptRequestParams = Infer; +export type GetPromptRequest = Infer; +export type TextContent = Infer; +export type ImageContent = Infer; +export type AudioContent = Infer; +export type ToolUseContent = Infer; +export type ToolResultContent = Infer; +export type EmbeddedResource = Infer; +export type ResourceLink = Infer; +export type ContentBlock = Infer; +export type PromptMessage = Infer; +export type GetPromptResult = Infer; +export type PromptListChangedNotification = Infer; + +/* Tools */ +export type ToolAnnotations = Infer; +export type ToolExecution = Infer; +export type Tool = Infer; +export type ListToolsRequest = Infer; +export type ListToolsResult = Infer; +export type CallToolRequestParams = Infer; +export type CallToolResult = Infer; +export type CompatibilityCallToolResult = Infer; +export type CallToolRequest = Infer; +export type ToolListChangedNotification = Infer; + +/* Logging */ +export type LoggingLevel = Infer; +export type SetLevelRequestParams = Infer; +export type SetLevelRequest = Infer; +export type LoggingMessageNotificationParams = Infer; +export type LoggingMessageNotification = Infer; + +/* Sampling */ +export type ToolChoice = Infer; +export type ModelHint = Infer; +export type ModelPreferences = Infer; +export type SamplingContent = Infer; +export type SamplingMessageContentBlock = Infer; +export type SamplingMessage = Infer; +export type CreateMessageRequestParams = Infer; +export type CreateMessageRequest = Infer; +export type CreateMessageResult = Infer; +export type CreateMessageResultWithTools = Infer; + +/* Elicitation */ +export type BooleanSchema = Infer; +export type StringSchema = Infer; +export type NumberSchema = Infer; +export type EnumSchema = Infer; +export type UntitledSingleSelectEnumSchema = Infer; +export type TitledSingleSelectEnumSchema = Infer; +export type LegacyTitledEnumSchema = Infer; +export type UntitledMultiSelectEnumSchema = Infer; +export type TitledMultiSelectEnumSchema = Infer; +export type SingleSelectEnumSchema = Infer; +export type MultiSelectEnumSchema = Infer; +export type PrimitiveSchemaDefinition = Infer; +export type ElicitRequestParams = Infer; +export type ElicitRequestFormParams = Infer; +export type ElicitRequestURLParams = Infer; +export type ElicitRequest = Infer; +export type ElicitationCompleteNotificationParams = Infer; +export type ElicitationCompleteNotification = Infer; +export type ElicitResult = Infer; + +/* Autocomplete */ +export type ResourceTemplateReference = Infer; +export type PromptReference = Infer; +export type CompleteRequestParams = Infer; +export type CompleteRequest = Infer; +export type CompleteResult = Infer; + +/* Roots */ +export type Root = Infer; +export type ListRootsRequest = Infer; +export type ListRootsResult = Infer; +export type RootsListChangedNotification = Infer; + +/* Client messages */ +export type ClientRequest = Infer; +export type ClientNotification = Infer; +export type ClientResult = Infer; + +/* Server messages */ +export type ServerRequest = Infer; +export type ServerNotification = Infer; +export type ServerResult = Infer; + +/* Protocol type maps */ +type MethodToTypeMap = { + [T in U as T extends { method: infer M extends string } ? M : never]: T; +}; +export type RequestMethod = ClientRequest['method'] | ServerRequest['method']; +export type NotificationMethod = ClientNotification['method'] | ServerNotification['method']; +export type RequestTypeMap = MethodToTypeMap; +export type NotificationTypeMap = MethodToTypeMap; +export type ResultTypeMap = { + ping: EmptyResult; + initialize: InitializeResult; + 'completion/complete': CompleteResult; + 'logging/setLevel': EmptyResult; + 'prompts/get': GetPromptResult; + 'prompts/list': ListPromptsResult; + 'resources/list': ListResourcesResult; + 'resources/templates/list': ListResourceTemplatesResult; + 'resources/read': ReadResourceResult; + 'resources/subscribe': EmptyResult; + 'resources/unsubscribe': EmptyResult; + 'tools/call': CallToolResult | CreateTaskResult; + 'tools/list': ListToolsResult; + 'sampling/createMessage': CreateMessageResult | CreateMessageResultWithTools | CreateTaskResult; + 'elicitation/create': ElicitResult | CreateTaskResult; + 'roots/list': ListRootsResult; + 'tasks/get': GetTaskResult; + 'tasks/result': Result; + 'tasks/list': ListTasksResult; + 'tasks/cancel': CancelTaskResult; +}; + +/** + * Information about a validated access token, provided to request handlers. + */ +export interface AuthInfo { + /** + * The access token. + */ + token: string; + + /** + * The client ID associated with this token. + */ + clientId: string; + + /** + * Scopes associated with this token. + */ + scopes: string[]; + + /** + * When the token expires (in seconds since epoch). + */ + expiresAt?: number; + + /** + * The RFC 8707 resource server identifier for which this token is valid. + * If set, this MUST match the MCP server's resource identifier (minus hash fragment). + */ + resource?: URL; + + /** + * Additional data associated with the token. + * This field should be used for any additional data that needs to be attached to the auth info. + */ + extra?: Record; +} + +type JSONRPCErrorObject = { code: number; message: string; data?: unknown }; + +export interface ParseError extends JSONRPCErrorObject { + code: typeof PARSE_ERROR; +} +export interface InvalidRequestError extends JSONRPCErrorObject { + code: typeof INVALID_REQUEST; +} +export interface MethodNotFoundError extends JSONRPCErrorObject { + code: typeof METHOD_NOT_FOUND; +} +export interface InvalidParamsError extends JSONRPCErrorObject { + code: typeof INVALID_PARAMS; +} +export interface InternalError extends JSONRPCErrorObject { + code: typeof INTERNAL_ERROR; +} + +/** + * Data carried by a `-32004` UnsupportedProtocolVersion protocol error + * (protocol revision 2026-07-28). + */ +export interface UnsupportedProtocolVersionErrorData { + /** + * Protocol versions the receiver supports. The sender should choose a + * mutually supported version from this list and retry. + */ + supported: string[]; + /** + * The protocol version that was requested. + */ + requested: string; +} + +/** + * Callback type for list changed notifications. + */ +export type ListChangedCallback = (error: Error | null, items: T[] | null) => void; + +/** + * Options for subscribing to list changed notifications. + * + * @typeParam T - The type of items in the list (`Tool`, `Prompt`, or `Resource`) + */ +export type ListChangedOptions = { + /** + * If `true`, the list will be refreshed automatically when a list changed notification is received. + * @default true + */ + autoRefresh?: boolean; + /** + * Debounce time in milliseconds. Set to `0` to disable. + * @default 300 + */ + debounceMs?: number; + /** + * Callback invoked when the list changes. + * + * If `autoRefresh` is `true`, `items` contains the updated list. + * If `autoRefresh` is `false`, `items` is `null` (caller should refresh manually). + */ + onChanged: ListChangedCallback; +}; + +/** + * Configuration for list changed notification handlers. + * + * Use this to configure handlers for tools, prompts, and resources list changes + * when creating a client. + * + * Note: Handlers are only activated if the server advertises the corresponding + * `listChanged` capability (e.g., `tools.listChanged: true`). If the server + * doesn't advertise this capability, the handler will not be set up. + */ +export type ListChangedHandlers = { + /** + * Handler for tool list changes. + */ + tools?: ListChangedOptions; + /** + * Handler for prompt list changes. + */ + prompts?: ListChangedOptions; + /** + * Handler for resource list changes. + */ + resources?: ListChangedOptions; +}; + +/** + * Extra information about a message. + */ +export interface MessageExtraInfo { + /** + * The original HTTP request. + */ + request?: globalThis.Request; + + /** + * The authentication information. + */ + authInfo?: AuthInfo; + + /** + * Callback to close the SSE stream for this request, triggering client reconnection. + * Only available when using {@linkcode @modelcontextprotocol/node!streamableHttp.NodeStreamableHTTPServerTransport | NodeStreamableHTTPServerTransport} with eventStore configured. + */ + closeSSEStream?: () => void; + + /** + * Callback to close the standalone GET SSE stream, triggering client reconnection. + * Only available when using {@linkcode @modelcontextprotocol/node!streamableHttp.NodeStreamableHTTPServerTransport | NodeStreamableHTTPServerTransport} with eventStore configured. + */ + closeStandaloneSSEStream?: () => void; +} + +export type MetaObject = Record; +export type RequestMetaObject = RequestMeta; + +/** + * {@linkcode CreateMessageRequestParams} without tools - for backwards-compatible overload. + * Excludes tools/toolChoice to indicate they should not be provided. + */ +export type CreateMessageRequestParamsBase = Omit; + +/** + * {@linkcode CreateMessageRequestParams} with required tools - for tool-enabled overload. + */ +export interface CreateMessageRequestParamsWithTools extends CreateMessageRequestParams { + tools: Tool[]; +} + +export type CompleteRequestResourceTemplate = ExpandRecursively< + CompleteRequest & { params: CompleteRequestParams & { ref: ResourceTemplateReference } } +>; +export type CompleteRequestPrompt = ExpandRecursively; diff --git a/packages/core-internal/src/util/inMemory.ts b/packages/core-internal/src/util/inMemory.ts new file mode 100644 index 0000000000..3afd2b1ac7 --- /dev/null +++ b/packages/core-internal/src/util/inMemory.ts @@ -0,0 +1,73 @@ +import { SdkError, SdkErrorCode } from '../errors/sdkErrors'; +import type { Transport } from '../shared/transport'; +import type { AuthInfo, JSONRPCMessage, RequestId } from '../types/index'; + +interface QueuedMessage { + message: JSONRPCMessage; + extra?: { authInfo?: AuthInfo }; +} + +/** + * In-memory transport for creating clients and servers that talk to each other within the same process. + * + * Intended for testing and development. For production in-process connections, use + * `StreamableHTTPClientTransport` against a local server URL. + */ +export class InMemoryTransport implements Transport { + private _otherTransport?: InMemoryTransport; + private _messageQueue: QueuedMessage[] = []; + private _closed = false; + + onclose?: () => void; + onerror?: (error: Error) => void; + onmessage?: (message: JSONRPCMessage, extra?: { authInfo?: AuthInfo }) => void; + sessionId?: string; + + /** + * Creates a pair of linked in-memory transports that can communicate with each other. One should be passed to a {@linkcode @modelcontextprotocol/client!client/client.Client | Client} and one to a {@linkcode @modelcontextprotocol/server!server/server.Server | Server}. + */ + static createLinkedPair(): [InMemoryTransport, InMemoryTransport] { + const clientTransport = new InMemoryTransport(); + const serverTransport = new InMemoryTransport(); + clientTransport._otherTransport = serverTransport; + serverTransport._otherTransport = clientTransport; + return [clientTransport, serverTransport]; + } + + async start(): Promise { + // Process any messages that were queued before start was called + while (this._messageQueue.length > 0) { + const queuedMessage = this._messageQueue.shift()!; + this.onmessage?.(queuedMessage.message, queuedMessage.extra); + } + } + + async close(): Promise { + if (this._closed) return; + this._closed = true; + + const other = this._otherTransport; + this._otherTransport = undefined; + try { + await other?.close(); + } finally { + this.onclose?.(); + } + } + + /** + * Sends a message with optional auth info. + * This is useful for testing authentication scenarios. + */ + async send(message: JSONRPCMessage, options?: { relatedRequestId?: RequestId; authInfo?: AuthInfo }): Promise { + if (!this._otherTransport) { + throw new SdkError(SdkErrorCode.NotConnected, 'Not connected'); + } + + if (this._otherTransport.onmessage) { + this._otherTransport.onmessage(message, { authInfo: options?.authInfo }); + } else { + this._otherTransport._messageQueue.push({ message, extra: { authInfo: options?.authInfo } }); + } + } +} diff --git a/packages/core-internal/src/util/schema.ts b/packages/core-internal/src/util/schema.ts new file mode 100644 index 0000000000..9676674b84 --- /dev/null +++ b/packages/core-internal/src/util/schema.ts @@ -0,0 +1,32 @@ +/** + * Internal Zod schema utilities for protocol handling. + * These are used internally by the SDK for protocol message validation. + */ + +import * as z from 'zod/v4'; + +/** + * Base type for any Zod schema. + */ +export type AnySchema = z.core.$ZodType; + +/** + * A Zod schema for objects specifically. + */ +export type AnyObjectSchema = z.core.$ZodObject; + +/** + * Extracts the output type from a Zod schema. + */ +export type SchemaOutput = z.output; + +/** + * Parses data against a Zod schema (synchronous). + * Returns a discriminated union with success/error. + */ +export function parseSchema( + schema: T, + data: unknown +): { success: true; data: z.output } | { success: false; error: z.core.$ZodError } { + return z.safeParse(schema, data); +} diff --git a/packages/core-internal/src/util/standardSchema.ts b/packages/core-internal/src/util/standardSchema.ts new file mode 100644 index 0000000000..b938885de0 --- /dev/null +++ b/packages/core-internal/src/util/standardSchema.ts @@ -0,0 +1,251 @@ +/** + * Standard Schema utilities for user-provided schemas. + * Supports Zod v4, Valibot, ArkType, and other Standard Schema implementations. + * @see https://standardschema.dev + */ + +/* eslint-disable @typescript-eslint/no-namespace */ + +import * as z from 'zod/v4'; + +// Standard Schema interfaces — vendored from https://standardschema.dev (spec v1, Jan 2025) + +export interface StandardTypedV1 { + readonly '~standard': StandardTypedV1.Props; +} + +export namespace StandardTypedV1 { + export interface Props { + readonly version: 1; + readonly vendor: string; + readonly types?: Types | undefined; + } + + export interface Types { + readonly input: Input; + readonly output: Output; + } + + export type InferInput = NonNullable['input']; + export type InferOutput = NonNullable['output']; +} + +export interface StandardSchemaV1 { + readonly '~standard': StandardSchemaV1.Props; +} + +export namespace StandardSchemaV1 { + export interface Props extends StandardTypedV1.Props { + readonly validate: (value: unknown, options?: Options | undefined) => Result | Promise>; + } + + export interface Options { + readonly libraryOptions?: Record | undefined; + } + + export type Result = SuccessResult | FailureResult; + + export interface SuccessResult { + readonly value: Output; + readonly issues?: undefined; + } + + export interface FailureResult { + readonly issues: ReadonlyArray; + } + + export interface Issue { + readonly message: string; + readonly path?: ReadonlyArray | undefined; + } + + export interface PathSegment { + readonly key: PropertyKey; + } + + export type InferInput = StandardTypedV1.InferInput; + export type InferOutput = StandardTypedV1.InferOutput; +} + +export interface StandardJSONSchemaV1 { + readonly '~standard': StandardJSONSchemaV1.Props; +} + +export namespace StandardJSONSchemaV1 { + export interface Props extends StandardTypedV1.Props { + readonly jsonSchema: Converter; + } + + export interface Converter { + readonly input: (options: Options) => Record; + readonly output: (options: Options) => Record; + } + + export type Target = 'draft-2020-12' | 'draft-07' | 'openapi-3.0' | (object & string); + + export interface Options { + readonly target: Target; + readonly libraryOptions?: Record | undefined; + } + + export type InferInput = StandardTypedV1.InferInput; + export type InferOutput = StandardTypedV1.InferOutput; +} + +/** + * Combined interface for schemas with both validation and JSON Schema conversion — + * the intersection of {@linkcode StandardSchemaV1} and {@linkcode StandardJSONSchemaV1}. + * + * This is the type accepted by `registerTool` / `registerPrompt`. The SDK needs + * `~standard.jsonSchema` to advertise the tool's argument shape in `tools/list`, and + * `~standard.validate` to check incoming arguments when a `tools/call` arrives. + * + * Zod v4, ArkType, and Valibot (via `@valibot/to-json-schema`'s `toStandardJsonSchema`) + * all implement both interfaces. + * + * @see https://standardschema.dev/ for the Standard Schema specification + */ +export interface StandardSchemaWithJSON { + readonly '~standard': StandardSchemaV1.Props & StandardJSONSchemaV1.Props; +} + +export namespace StandardSchemaWithJSON { + export type InferInput = StandardTypedV1.InferInput; + export type InferOutput = StandardTypedV1.InferOutput; +} + +/** + * Narrowing of {@linkcode StandardSchemaV1} whose `validate` is guaranteed synchronous. + * + * The Zod schemas backing `specTypeSchemas` contain no async refinements or transforms, + * so every entry satisfies this interface. Consumers can call `validate()` and access + * `.issues` / `.value` on the result without `await`. + * + * `StandardSchemaV1Sync` is assignable to `StandardSchemaV1` — it is a strict subtype. + */ +export interface StandardSchemaV1Sync extends StandardSchemaV1 { + readonly '~standard': StandardSchemaV1Sync.Props; +} + +export namespace StandardSchemaV1Sync { + export interface Props extends StandardSchemaV1.Props { + readonly validate: (value: unknown, options?: StandardSchemaV1.Options | undefined) => StandardSchemaV1.Result; + } + + export type InferInput = StandardTypedV1.InferInput; + export type InferOutput = StandardTypedV1.InferOutput; +} + +// Type guards + +export function isStandardJSONSchema(schema: unknown): schema is StandardJSONSchemaV1 { + if (schema == null) return false; + const schemaType = typeof schema; + if (schemaType !== 'object' && schemaType !== 'function') return false; + if (!('~standard' in (schema as object))) return false; + const std = (schema as StandardJSONSchemaV1)['~standard']; + return typeof std?.jsonSchema?.input === 'function' && typeof std?.jsonSchema?.output === 'function'; +} + +export function isStandardSchema(schema: unknown): schema is StandardSchemaV1 { + if (schema == null) return false; + const schemaType = typeof schema; + if (schemaType !== 'object' && schemaType !== 'function') return false; + if (!('~standard' in (schema as object))) return false; + const std = (schema as StandardSchemaV1)['~standard']; + return typeof std?.validate === 'function'; +} + +export function isStandardSchemaWithJSON(schema: unknown): schema is StandardSchemaWithJSON { + return isStandardJSONSchema(schema) && isStandardSchema(schema); +} + +// JSON Schema conversion + +let warnedZodFallback = false; + +/** + * Converts a StandardSchema to JSON Schema for use as an MCP tool/prompt schema. + * + * MCP requires `type: "object"` at the root of tool inputSchema/outputSchema and + * prompt argument schemas. Zod's discriminated unions emit `{oneOf: [...]}` without + * a top-level `type`, so this function defaults `type` to `"object"` when absent. + * + * Throws if the schema has an explicit non-object `type` (e.g. `z.string()`), + * since that cannot satisfy the MCP spec. + */ +export function standardSchemaToJsonSchema(schema: StandardJSONSchemaV1, io: 'input' | 'output' = 'input'): Record { + const std = schema['~standard']; + let result: Record; + if (std.jsonSchema) { + result = std.jsonSchema[io]({ target: 'draft-2020-12' }); + } else if (std.vendor === 'zod') { + // zod 4.0–4.1 implements StandardSchemaV1 but not StandardJSONSchemaV1 (`~standard.jsonSchema`). + // The SDK already bundles zod 4, so fall back to its converter rather than crashing on tools/list. + // zod 3 schemas (which also report vendor 'zod') have `_def` but not `_zod`; the SDK-bundled + // zod 4 `z.toJSONSchema()` cannot introspect them, so throw a clear error instead of crashing. + if (!('_zod' in (schema as object))) { + throw new Error( + 'Schema appears to be from zod 3, which the SDK cannot convert to JSON Schema. ' + + 'Upgrade to zod >=4.2.0, or wrap your JSON Schema with fromJsonSchema().' + ); + } + if (!warnedZodFallback) { + warnedZodFallback = true; + console.warn( + '[mcp-sdk] Your zod version does not implement `~standard.jsonSchema` (added in zod 4.2.0). ' + + 'Falling back to z.toJSONSchema(). Upgrade to zod >=4.2.0 to silence this warning.' + ); + } + result = z.toJSONSchema(schema as unknown as z.ZodType, { target: 'draft-2020-12', io }) as Record; + } else { + throw new Error( + `Schema library "${std.vendor}" does not implement StandardJSONSchemaV1 (\`~standard.jsonSchema\`). ` + + `Upgrade to a version that does, or wrap your JSON Schema with fromJsonSchema().` + ); + } + if (result.type !== undefined && result.type !== 'object') { + throw new Error( + `MCP tool and prompt schemas must describe objects (got type: ${JSON.stringify(result.type)}). ` + + `Wrap your schema in z.object({...}) or equivalent.` + ); + } + return { type: 'object', ...result }; +} + +// Validation + +export type StandardSchemaValidationResult = { success: true; data: T } | { success: false; error: string }; + +function formatIssue(issue: StandardSchemaV1.Issue): string { + if (!issue.path?.length) return issue.message; + const path = issue.path.map(p => String(typeof p === 'object' ? p.key : p)).join('.'); + return `${path}: ${issue.message}`; +} + +export async function validateStandardSchema( + schema: T, + data: unknown +): Promise>> { + const result = await schema['~standard'].validate(data); + if (result.issues && result.issues.length > 0) { + return { success: false, error: result.issues.map(i => formatIssue(i)).join(', ') }; + } + return { success: true, data: (result as StandardSchemaV1.SuccessResult).value as StandardSchemaV1.InferOutput }; +} + +// Prompt argument extraction + +export function promptArgumentsFromStandardSchema( + schema: StandardJSONSchemaV1 +): Array<{ name: string; description?: string; required: boolean }> { + const jsonSchema = standardSchemaToJsonSchema(schema, 'input'); + const properties = (jsonSchema.properties as Record) || {}; + const required = (jsonSchema.required as string[]) || []; + + return Object.entries(properties).map(([name, prop]) => ({ + name, + description: prop?.description, + required: required.includes(name) + })); +} diff --git a/packages/core-internal/src/util/zodCompat.ts b/packages/core-internal/src/util/zodCompat.ts new file mode 100644 index 0000000000..249dba5154 --- /dev/null +++ b/packages/core-internal/src/util/zodCompat.ts @@ -0,0 +1,80 @@ +/** + * Zod-specific helpers for the v1-compat raw-shape shorthand on + * `registerTool`/`registerPrompt`. Kept separate from `standardSchema.ts` so + * that file stays library-agnostic per the Standard Schema spec. + */ + +import * as z from 'zod/v4'; + +import type { StandardSchemaWithJSON } from './standardSchema'; +import { isStandardSchema } from './standardSchema'; + +function isZodV4Schema(v: unknown): v is z.ZodType { + // `_zod` is the v4 internal namespace property. Zod v3 schemas have `_def` + // and (since 3.24) `~standard.vendor === 'zod'`, but never `_zod`. We require + // v4 because the wrap path below uses v4's `z.object()`, which cannot consume + // v3 field schemas. + return typeof v === 'object' && v !== null && '_zod' in v; +} + +function looksLikeZodV3(v: unknown): boolean { + // v3 schemas have `_def.typeName` (e.g. 'ZodString') and no `_zod`. + return ( + typeof v === 'object' && + v !== null && + !('_zod' in v) && + '_def' in v && + typeof (v as { _def?: { typeName?: unknown } })._def?.typeName === 'string' + ); +} + +/** + * Detects a "raw shape" — a plain object whose values are Zod field schemas, + * e.g. `{ name: z.string() }`. Powers the auto-wrap in + * {@linkcode normalizeRawShapeSchema}, which wraps with `z.object()`, so only + * Zod values are supported. + * + * @internal + */ +export function isZodRawShape(obj: unknown): obj is Record { + if (typeof obj !== 'object' || obj === null) return false; + if (isStandardSchema(obj)) return false; + // Require a plain object literal: rejects arrays, Date, Map, RegExp, class instances, etc. + // Object.create(null) is also accepted. + const proto = Object.getPrototypeOf(obj); + if (proto !== Object.prototype && proto !== null) return false; + // [].every() is true, so an empty plain object is a valid raw shape (matches v1). + return Object.values(obj).every(v => isZodV4Schema(v)); +} + +/** + * Accepts either a {@linkcode StandardSchemaWithJSON} or a raw Zod shape + * `{ field: z.string() }` and returns a {@linkcode StandardSchemaWithJSON}. + * Raw shapes are wrapped with `z.object()` so the rest of the pipeline sees a + * uniform schema type; already-wrapped schemas pass through unchanged. + * + * @internal + */ +export function normalizeRawShapeSchema( + schema: StandardSchemaWithJSON | Record | undefined +): StandardSchemaWithJSON | undefined { + if (schema === undefined) return undefined; + if (isZodRawShape(schema)) { + return z.object(schema) as StandardSchemaWithJSON; + } + if (typeof schema === 'object' && schema !== null && !isStandardSchema(schema) && Object.values(schema).some(v => looksLikeZodV3(v))) { + throw new TypeError( + 'Raw-shape inputSchema/outputSchema/argsSchema fields must be Zod v4 schemas. Got a Zod v3 field schema. Import from `zod/v4` (or upgrade your zod import), or wrap with `z.object({...})` yourself.' + ); + } + if (!isStandardSchema(schema)) { + throw new TypeError( + 'inputSchema/outputSchema/argsSchema must be a Standard Schema (e.g. z.object({...})) or a raw Zod shape ({ field: z.string() }).' + ); + } + // Any StandardSchema passes through; standardSchemaToJsonSchema owns the per-vendor + // handling for schemas without `~standard.jsonSchema` (zod 4.0-4.1 fallback, zod 3 + // and non-zod errors). Gating on `~standard.jsonSchema` here would unreachably + // front-run that fallback. + return schema; +} diff --git a/packages/core-internal/src/validators/ajvProvider.examples.ts b/packages/core-internal/src/validators/ajvProvider.examples.ts new file mode 100644 index 0000000000..923d5a68ba --- /dev/null +++ b/packages/core-internal/src/validators/ajvProvider.examples.ts @@ -0,0 +1,46 @@ +/** + * Type-checked examples for `ajvProvider.ts`. + * + * These examples are synced into JSDoc comments via the sync-snippets script. + * Each function's region markers define the code snippet that appears in the docs. + * + * @module + */ + +import { addFormats, Ajv, AjvJsonSchemaValidator } from './ajvProvider'; + +/** + * Example: Default AJV instance. + */ +function AjvJsonSchemaValidator_default() { + //#region AjvJsonSchemaValidator_default + const validator = new AjvJsonSchemaValidator(); + //#endregion AjvJsonSchemaValidator_default + return validator; +} + +/** + * Example: Custom AJV instance. + */ +function AjvJsonSchemaValidator_customInstance() { + //#region AjvJsonSchemaValidator_customInstance + const ajv = new Ajv({ strict: true, allErrors: true }); + const validator = new AjvJsonSchemaValidator(ajv); + //#endregion AjvJsonSchemaValidator_customInstance + return validator; +} + +/** + * Example: Custom AJV instance with formats registered. + * + * `Ajv` and `addFormats` are re-exported from this module so customising the validator + * requires no extra `package.json` dependencies — both come from the SDK's bundled copy. + */ +function AjvJsonSchemaValidator_withFormats() { + //#region AjvJsonSchemaValidator_withFormats + const ajv = new Ajv({ strict: true, allErrors: true }); + addFormats(ajv); + const validator = new AjvJsonSchemaValidator(ajv); + //#endregion AjvJsonSchemaValidator_withFormats + return validator; +} diff --git a/packages/core-internal/src/validators/ajvProvider.ts b/packages/core-internal/src/validators/ajvProvider.ts new file mode 100644 index 0000000000..23d64dac7c --- /dev/null +++ b/packages/core-internal/src/validators/ajvProvider.ts @@ -0,0 +1,99 @@ +/** + * AJV-based JSON Schema validator provider + */ + +import { Ajv } from 'ajv'; +import _addFormats from 'ajv-formats'; + +import type { JsonSchemaType, JsonSchemaValidator, jsonSchemaValidator, JsonSchemaValidatorResult } from './types'; + +/** Structural subset of the AJV interface used by {@link AjvJsonSchemaValidator}. */ +interface AjvLike { + compile: (schema: unknown) => AjvValidateFunction; + getSchema: (keyRef: string) => AjvValidateFunction | undefined; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + errorsText: (errors?: any) => string; +} + +interface AjvValidateFunction { + (input: unknown): boolean; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + errors?: any; +} + +function createDefaultAjvInstance(): Ajv { + const ajv = new Ajv({ + strict: false, + validateFormats: true, + validateSchema: false, + allErrors: true + }); + + const addFormats = _addFormats as unknown as typeof _addFormats.default; + addFormats(ajv); + + return ajv; +} + +/** + * AJV-backed JSON Schema validator. See `@modelcontextprotocol/{client,server}/validators/ajv` + * for the customisation entry point (re-exports `Ajv` and `addFormats` from the bundled copy). + * + * @example Use with default configuration + * ```ts source="./ajvProvider.examples.ts#AjvJsonSchemaValidator_default" + * const validator = new AjvJsonSchemaValidator(); + * ``` + * + * @example Use with a custom AJV instance + * ```ts source="./ajvProvider.examples.ts#AjvJsonSchemaValidator_customInstance" + * const ajv = new Ajv({ strict: true, allErrors: true }); + * const validator = new AjvJsonSchemaValidator(ajv); + * ``` + * + * @example Register ajv-formats + * ```ts source="./ajvProvider.examples.ts#AjvJsonSchemaValidator_withFormats" + * const ajv = new Ajv({ strict: true, allErrors: true }); + * addFormats(ajv); + * const validator = new AjvJsonSchemaValidator(ajv); + * ``` + */ +export class AjvJsonSchemaValidator implements jsonSchemaValidator { + private _ajv: AjvLike; + + /** + * @param ajv - Optional pre-configured AJV-compatible instance. If omitted, a default instance is + * created with `strict: false`, `validateFormats: true`, `validateSchema: false`, `allErrors: true`, + * and `ajv-formats` registered. The parameter is typed structurally so consumers who don't pass + * an instance need not have `ajv` installed. + */ + constructor(ajv?: AjvLike) { + this._ajv = ajv ?? createDefaultAjvInstance(); + } + + getValidator(schema: JsonSchemaType): JsonSchemaValidator { + const ajvValidator = + '$id' in schema && typeof schema.$id === 'string' + ? (this._ajv.getSchema(schema.$id) ?? this._ajv.compile(schema)) + : this._ajv.compile(schema); + + return (input: unknown): JsonSchemaValidatorResult => { + const valid = ajvValidator(input); + + return valid + ? { + valid: true, + data: input as T, + errorMessage: undefined + } + : { + valid: false, + data: undefined, + errorMessage: this._ajv.errorsText(ajvValidator.errors) + }; + }; + } +} + +export { Ajv } from 'ajv'; +/** `ajv-formats` default export, normalised through the CJS/ESM interop wrapper. */ +export const addFormats = _addFormats as unknown as typeof _addFormats.default; diff --git a/packages/core-internal/src/validators/cfWorkerProvider.examples.ts b/packages/core-internal/src/validators/cfWorkerProvider.examples.ts new file mode 100644 index 0000000000..facc971b0c --- /dev/null +++ b/packages/core-internal/src/validators/cfWorkerProvider.examples.ts @@ -0,0 +1,33 @@ +/** + * Type-checked examples for `cfWorkerProvider.ts`. + * + * These examples are synced into JSDoc comments via the sync-snippets script. + * Each function's region markers define the code snippet that appears in the docs. + * + * @module + */ + +import { CfWorkerJsonSchemaValidator } from './cfWorkerProvider'; + +/** + * Example: Default configuration (draft 2020-12, shortcircuit on). + */ +function CfWorkerJsonSchemaValidator_default() { + //#region CfWorkerJsonSchemaValidator_default + const validator = new CfWorkerJsonSchemaValidator(); + //#endregion CfWorkerJsonSchemaValidator_default + return validator; +} + +/** + * Example: Custom configuration with all errors reported. + */ +function CfWorkerJsonSchemaValidator_customConfig() { + //#region CfWorkerJsonSchemaValidator_customConfig + const validator = new CfWorkerJsonSchemaValidator({ + draft: '2020-12', + shortcircuit: false // Report all errors + }); + //#endregion CfWorkerJsonSchemaValidator_customConfig + return validator; +} diff --git a/packages/core-internal/src/validators/cfWorkerProvider.ts b/packages/core-internal/src/validators/cfWorkerProvider.ts new file mode 100644 index 0000000000..c3cfb34481 --- /dev/null +++ b/packages/core-internal/src/validators/cfWorkerProvider.ts @@ -0,0 +1,81 @@ +/** + * Cloudflare Worker-compatible JSON Schema validator provider + * + * This provider uses @cfworker/json-schema for validation without code generation, + * making it compatible with edge runtimes like Cloudflare Workers that restrict + * eval and new Function. + * + * @see {@linkcode AjvJsonSchemaValidator} for the Node.js alternative + */ + +import { Validator } from '@cfworker/json-schema'; + +import type { JsonSchemaType, JsonSchemaValidator, jsonSchemaValidator, JsonSchemaValidatorResult } from './types'; + +/** + * JSON Schema draft version supported by `@cfworker/json-schema`. + */ +export type CfWorkerSchemaDraft = '4' | '7' | '2019-09' | '2020-12'; + +/** + * `@cfworker/json-schema`-backed JSON Schema validator. See + * `@modelcontextprotocol/{client,server}/validators/cf-worker` for the customisation entry point. + * + * @example Use with default configuration (draft 2020-12, shortcircuit on) + * ```ts source="./cfWorkerProvider.examples.ts#CfWorkerJsonSchemaValidator_default" + * const validator = new CfWorkerJsonSchemaValidator(); + * ``` + * + * @example Use with custom configuration + * ```ts source="./cfWorkerProvider.examples.ts#CfWorkerJsonSchemaValidator_customConfig" + * const validator = new CfWorkerJsonSchemaValidator({ + * draft: '2020-12', + * shortcircuit: false // Report all errors + * }); + * ``` + */ +export class CfWorkerJsonSchemaValidator implements jsonSchemaValidator { + private shortcircuit: boolean; + private draft: CfWorkerSchemaDraft; + + /** + * Create a validator + * + * @param options - Configuration options + * @param options.shortcircuit - If `true`, stop validation after first error (default: `true`) + * @param options.draft - JSON Schema draft version to use (default: `'2020-12'`) + */ + constructor(options?: { shortcircuit?: boolean; draft?: CfWorkerSchemaDraft }) { + this.shortcircuit = options?.shortcircuit ?? true; + this.draft = options?.draft ?? '2020-12'; + } + + /** + * Create a validator for the given JSON Schema + * + * Unlike AJV, this validator is not cached internally + * + * @param schema - Standard JSON Schema object + * @returns A validator function that validates input data + */ + getValidator(schema: JsonSchemaType): JsonSchemaValidator { + // Cast to the cfworker Schema type - our JsonSchemaType is structurally compatible + const validator = new Validator(schema as ConstructorParameters[0], this.draft, this.shortcircuit); + + return (input: unknown): JsonSchemaValidatorResult => { + const result = validator.validate(input); + + return result.valid + ? { + valid: true, + data: input as T, + errorMessage: undefined + } + : { + valid: false, + data: undefined, + errorMessage: result.errors.map(err => `${err.instanceLocation}: ${err.error}`).join('; ') + }; + }; + } +} diff --git a/packages/core-internal/src/validators/fromJsonSchema.examples.ts b/packages/core-internal/src/validators/fromJsonSchema.examples.ts new file mode 100644 index 0000000000..7df661c545 --- /dev/null +++ b/packages/core-internal/src/validators/fromJsonSchema.examples.ts @@ -0,0 +1,30 @@ +/** + * Type-checked examples for `fromJsonSchema.ts`. + * + * These examples are synced into JSDoc comments via the sync-snippets script. + * + * @module + */ + +import { fromJsonSchema } from './fromJsonSchema'; +import type { jsonSchemaValidator } from './types'; + +declare const validator: jsonSchemaValidator; + +/** + * Example: wrap a raw JSON Schema object for use with registerTool. + * + * Consumers importing `fromJsonSchema` from `@modelcontextprotocol/server` or + * `@modelcontextprotocol/client` omit the second argument — the runtime shim + * supplies the appropriate default validator. + */ +function fromJsonSchema_basicUsage() { + //#region fromJsonSchema_basicUsage + const inputSchema = fromJsonSchema<{ name: string }>( + { type: 'object', properties: { name: { type: 'string' } }, required: ['name'] }, + validator + ); + // Use with server.registerTool('greet', { inputSchema }, handler) + //#endregion fromJsonSchema_basicUsage + return inputSchema; +} diff --git a/packages/core-internal/src/validators/fromJsonSchema.ts b/packages/core-internal/src/validators/fromJsonSchema.ts new file mode 100644 index 0000000000..696d1f29e9 --- /dev/null +++ b/packages/core-internal/src/validators/fromJsonSchema.ts @@ -0,0 +1,43 @@ +import type { StandardSchemaV1, StandardSchemaWithJSON } from '../util/standardSchema'; +import type { JsonSchemaType, jsonSchemaValidator } from './types'; + +/** + * Wrap a raw JSON Schema object as a {@linkcode StandardSchemaWithJSON} so it can be + * passed to `registerTool` / `registerPrompt`. Use this when you already have JSON + * Schema (e.g. from TypeBox, or hand-written) and want to register it without going + * through a Standard Schema library. + * + * The callback arguments will be typed `unknown` (raw JSON Schema has no TypeScript + * types attached). Cast at the call site, or use the generic `fromJsonSchema(...)`. + * + * @param schema - A JSON Schema object describing the expected shape + * @param validator - A validator provider. When importing `fromJsonSchema` from + * `@modelcontextprotocol/server` or `@modelcontextprotocol/client`, a runtime-appropriate + * default is provided automatically (AJV on Node.js, CfWorker on edge runtimes). + * + * @example + * ```ts source="./fromJsonSchema.examples.ts#fromJsonSchema_basicUsage" + * const inputSchema = fromJsonSchema<{ name: string }>( + * { type: 'object', properties: { name: { type: 'string' } }, required: ['name'] }, + * validator + * ); + * // Use with server.registerTool('greet', { inputSchema }, handler) + * ``` + */ +export function fromJsonSchema(schema: JsonSchemaType, validator: jsonSchemaValidator): StandardSchemaWithJSON { + const check = validator.getValidator(schema); + return { + '~standard': { + version: 1, + vendor: 'mcp', + jsonSchema: { + input: () => schema as Record, + output: () => schema as Record + }, + validate: (data: unknown): StandardSchemaV1.Result => { + const result = check(data); + return result.valid ? { value: result.data } : { issues: [{ message: result.errorMessage }] }; + } + } + }; +} diff --git a/packages/core-internal/src/validators/types.examples.ts b/packages/core-internal/src/validators/types.examples.ts new file mode 100644 index 0000000000..2066a8aff3 --- /dev/null +++ b/packages/core-internal/src/validators/types.examples.ts @@ -0,0 +1,31 @@ +/** + * Type-checked examples for `types.ts`. + * + * These examples are synced into JSDoc comments via the sync-snippets script. + * Each function's region markers define the code snippet that appears in the docs. + * + * @module + */ + +import type { JsonSchemaType, JsonSchemaValidator, jsonSchemaValidator } from './types'; + +// Stub for hypothetical schema validation function +declare function isValid(schema: JsonSchemaType, input: unknown): boolean; + +/** + * Example: Implementing the jsonSchemaValidator interface. + */ +function jsonSchemaValidator_implementation() { + //#region jsonSchemaValidator_implementation + class MyValidatorProvider implements jsonSchemaValidator { + getValidator(schema: JsonSchemaType): JsonSchemaValidator { + // Compile/cache validator from schema + return (input: unknown) => + isValid(schema, input) + ? { valid: true, data: input as T, errorMessage: undefined } + : { valid: false, data: undefined, errorMessage: 'Error details' }; + } + } + //#endregion jsonSchemaValidator_implementation + return MyValidatorProvider; +} diff --git a/packages/core-internal/src/validators/types.ts b/packages/core-internal/src/validators/types.ts new file mode 100644 index 0000000000..e2202b4a69 --- /dev/null +++ b/packages/core-internal/src/validators/types.ts @@ -0,0 +1,59 @@ +// Using the main export which points to draft-2020-12 by default +import type { JSONSchema } from 'json-schema-typed'; + +/** + * JSON Schema type definition (JSON Schema Draft 2020-12) + * + * This uses the object form of JSON Schema (excluding boolean schemas). + * While `true` and `false` are valid JSON Schemas, this SDK uses the + * object form for practical type safety. + * + * Re-exported from json-schema-typed for convenience. + * @see https://json-schema.org/draft/2020-12/json-schema-core.html + */ +export type JsonSchemaType = JSONSchema.Interface; + +/** + * Result of a JSON Schema validation operation + */ +export type JsonSchemaValidatorResult = + | { valid: true; data: T; errorMessage: undefined } + | { valid: false; data: undefined; errorMessage: string }; + +/** + * A validator function that validates data against a JSON Schema + */ +export type JsonSchemaValidator = (input: unknown) => JsonSchemaValidatorResult; + +/** + * Provider interface for creating validators from JSON Schemas + * + * This is the main extension point for custom validator implementations. + * Implementations should: + * - Support JSON Schema Draft 2020-12 (or be compatible with it) + * - Return validator functions that can be called multiple times + * - Handle schema compilation/caching internally + * - Provide clear error messages on validation failure + * + * @example + * ```ts source="./types.examples.ts#jsonSchemaValidator_implementation" + * class MyValidatorProvider implements jsonSchemaValidator { + * getValidator(schema: JsonSchemaType): JsonSchemaValidator { + * // Compile/cache validator from schema + * return (input: unknown) => + * isValid(schema, input) + * ? { valid: true, data: input as T, errorMessage: undefined } + * : { valid: false, data: undefined, errorMessage: 'Error details' }; + * } + * } + * ``` + */ +export interface jsonSchemaValidator { + /** + * Create a validator for the given JSON Schema + * + * @param schema - Standard JSON Schema object + * @returns A validator function that can be called multiple times + */ + getValidator(schema: JsonSchemaType): JsonSchemaValidator; +} diff --git a/packages/core-internal/test/errors/sdkHttpError.test.ts b/packages/core-internal/test/errors/sdkHttpError.test.ts new file mode 100644 index 0000000000..421a51d6c6 --- /dev/null +++ b/packages/core-internal/test/errors/sdkHttpError.test.ts @@ -0,0 +1,55 @@ +import { describe, it, expect } from 'vitest'; +import { SdkError, SdkErrorCode, SdkHttpError } from '../../src/index'; + +describe('SdkHttpError', () => { + it('exposes status and statusText via getters', () => { + const error = new SdkHttpError(SdkErrorCode.ClientHttpAuthentication, 'Unauthorized', { + status: 401, + statusText: 'Unauthorized' + }); + + expect(error.status).toBe(401); + expect(error.statusText).toBe('Unauthorized'); + }); + + it('returns undefined for statusText when omitted', () => { + const error = new SdkHttpError(SdkErrorCode.ClientHttpAuthentication, 'auth failed', { + status: 401 + }); + + expect(error.status).toBe(401); + expect(error.statusText).toBeUndefined(); + }); + + it('is an instance of SdkError', () => { + const error = new SdkHttpError(SdkErrorCode.ClientHttpForbidden, 'Forbidden', { + status: 403, + statusText: 'Forbidden' + }); + + expect(error).toBeInstanceOf(SdkError); + expect(error).toBeInstanceOf(SdkHttpError); + }); + + it('preserves code and message from SdkError', () => { + const error = new SdkHttpError(SdkErrorCode.ClientHttpNotImplemented, 'Not Implemented', { + status: 501, + statusText: 'Not Implemented' + }); + + expect(error.code).toBe(SdkErrorCode.ClientHttpNotImplemented); + expect(error.message).toBe('Not Implemented'); + expect(error.name).toBe('SdkHttpError'); + }); + + it('exposes extra data fields alongside status', () => { + const error = new SdkHttpError(SdkErrorCode.ClientHttpAuthentication, 'auth failed', { + status: 401, + statusText: 'Unauthorized', + retryAfter: 30 + }); + + expect(error.data.retryAfter).toBe(30); + expect(error.status).toBe(401); + }); +}); diff --git a/packages/core-internal/test/inMemory.test.ts b/packages/core-internal/test/inMemory.test.ts new file mode 100644 index 0000000000..0dc72fd32f --- /dev/null +++ b/packages/core-internal/test/inMemory.test.ts @@ -0,0 +1,165 @@ +import type { AuthInfo, JSONRPCMessage } from '../src/types/index'; +import { InMemoryTransport } from '../src/util/inMemory'; + +describe('InMemoryTransport', () => { + let clientTransport: InMemoryTransport; + let serverTransport: InMemoryTransport; + + beforeEach(() => { + [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); + }); + + test('should create linked pair', () => { + expect(clientTransport).toBeDefined(); + expect(serverTransport).toBeDefined(); + }); + + test('should start without error', async () => { + await expect(clientTransport.start()).resolves.not.toThrow(); + await expect(serverTransport.start()).resolves.not.toThrow(); + }); + + test('should send message from client to server', async () => { + const message: JSONRPCMessage = { + jsonrpc: '2.0', + method: 'test', + id: 1 + }; + + let receivedMessage: JSONRPCMessage | undefined; + serverTransport.onmessage = msg => { + receivedMessage = msg; + }; + + await clientTransport.send(message); + expect(receivedMessage).toEqual(message); + }); + + test('should send message with auth info from client to server', async () => { + const message: JSONRPCMessage = { + jsonrpc: '2.0', + method: 'test', + id: 1 + }; + + const authInfo: AuthInfo = { + token: 'test-token', + clientId: 'test-client', + scopes: ['read', 'write'], + expiresAt: Date.now() / 1000 + 3600 + }; + + let receivedMessage: JSONRPCMessage | undefined; + let receivedAuthInfo: AuthInfo | undefined; + serverTransport.onmessage = (msg, extra) => { + receivedMessage = msg; + receivedAuthInfo = extra?.authInfo; + }; + + await clientTransport.send(message, { authInfo }); + expect(receivedMessage).toEqual(message); + expect(receivedAuthInfo).toEqual(authInfo); + }); + + test('should send message from server to client', async () => { + const message: JSONRPCMessage = { + jsonrpc: '2.0', + method: 'test', + id: 1 + }; + + let receivedMessage: JSONRPCMessage | undefined; + clientTransport.onmessage = msg => { + receivedMessage = msg; + }; + + await serverTransport.send(message); + expect(receivedMessage).toEqual(message); + }); + + test('should handle close', async () => { + let clientClosed = false; + let serverClosed = false; + + clientTransport.onclose = () => { + clientClosed = true; + }; + + serverTransport.onclose = () => { + serverClosed = true; + }; + + await clientTransport.close(); + expect(clientClosed).toBe(true); + expect(serverClosed).toBe(true); + }); + + test('should throw error when sending after close', async () => { + await clientTransport.close(); + await expect(clientTransport.send({ jsonrpc: '2.0', method: 'test', id: 1 })).rejects.toThrow('Not connected'); + }); + + test('should fire onclose exactly once per transport', async () => { + let clientCloseCount = 0; + let serverCloseCount = 0; + + clientTransport.onclose = () => clientCloseCount++; + serverTransport.onclose = () => serverCloseCount++; + + await clientTransport.close(); + + expect(clientCloseCount).toBe(1); + expect(serverCloseCount).toBe(1); + }); + + test('should handle double close idempotently', async () => { + let clientCloseCount = 0; + clientTransport.onclose = () => clientCloseCount++; + + await clientTransport.close(); + await clientTransport.close(); + + expect(clientCloseCount).toBe(1); + }); + + test('should handle concurrent close from both sides', async () => { + let clientCloseCount = 0; + let serverCloseCount = 0; + + clientTransport.onclose = () => clientCloseCount++; + serverTransport.onclose = () => serverCloseCount++; + + await Promise.all([clientTransport.close(), serverTransport.close()]); + + expect(clientCloseCount).toBe(1); + expect(serverCloseCount).toBe(1); + }); + + test('should fire onclose even if peer onclose throws', async () => { + let clientCloseCount = 0; + clientTransport.onclose = () => clientCloseCount++; + serverTransport.onclose = () => { + throw new Error('boom'); + }; + + await expect(clientTransport.close()).rejects.toThrow('boom'); + expect(clientCloseCount).toBe(1); + }); + + test('should queue messages sent before start', async () => { + const message: JSONRPCMessage = { + jsonrpc: '2.0', + method: 'test', + id: 1 + }; + + let receivedMessage: JSONRPCMessage | undefined; + serverTransport.onmessage = msg => { + receivedMessage = msg; + }; + + await clientTransport.send(message); + await serverTransport.start(); + expect(receivedMessage).toEqual(message); + }); +}); diff --git a/packages/core-internal/test/shared/auth.test.ts b/packages/core-internal/test/shared/auth.test.ts new file mode 100644 index 0000000000..10c9462c9a --- /dev/null +++ b/packages/core-internal/test/shared/auth.test.ts @@ -0,0 +1,122 @@ +import { + OAuthClientMetadataSchema, + OAuthMetadataSchema, + OpenIdProviderMetadataSchema, + OptionalSafeUrlSchema, + SafeUrlSchema +} from '../../src/shared/auth'; + +describe('SafeUrlSchema', () => { + it('accepts valid HTTPS URLs', () => { + expect(SafeUrlSchema.parse('https://example.com')).toBe('https://example.com'); + expect(SafeUrlSchema.parse('https://auth.example.com/oauth/authorize')).toBe('https://auth.example.com/oauth/authorize'); + }); + + it('accepts valid HTTP URLs', () => { + expect(SafeUrlSchema.parse('http://localhost:3000')).toBe('http://localhost:3000'); + }); + + it('rejects javascript: scheme URLs', () => { + expect(() => SafeUrlSchema.parse('javascript:alert(1)')).toThrow('URL cannot use javascript:, data:, or vbscript: scheme'); + expect(() => SafeUrlSchema.parse('JAVASCRIPT:alert(1)')).toThrow('URL cannot use javascript:, data:, or vbscript: scheme'); + }); + + it('rejects invalid URLs', () => { + expect(() => SafeUrlSchema.parse('not-a-url')).toThrow(); + expect(() => SafeUrlSchema.parse('')).toThrow(); + }); + + it('works with safeParse', () => { + expect(() => SafeUrlSchema.safeParse('not-a-url')).not.toThrow(); + }); +}); + +describe('OptionalSafeUrlSchema', () => { + it('accepts empty string and transforms it to undefined', () => { + expect(OptionalSafeUrlSchema.parse('')).toBe(undefined); + }); +}); + +describe('OAuthMetadataSchema', () => { + it('validates complete OAuth metadata', () => { + const metadata = { + issuer: 'https://auth.example.com', + authorization_endpoint: 'https://auth.example.com/oauth/authorize', + token_endpoint: 'https://auth.example.com/oauth/token', + response_types_supported: ['code'], + scopes_supported: ['read', 'write'] + }; + + expect(() => OAuthMetadataSchema.parse(metadata)).not.toThrow(); + }); + + it('rejects metadata with javascript: URLs', () => { + const metadata = { + issuer: 'https://auth.example.com', + authorization_endpoint: 'javascript:alert(1)', + token_endpoint: 'https://auth.example.com/oauth/token', + response_types_supported: ['code'] + }; + + expect(() => OAuthMetadataSchema.parse(metadata)).toThrow('URL cannot use javascript:, data:, or vbscript: scheme'); + }); + + it('requires mandatory fields', () => { + const incompleteMetadata = { + issuer: 'https://auth.example.com' + }; + + expect(() => OAuthMetadataSchema.parse(incompleteMetadata)).toThrow(); + }); +}); + +describe('OpenIdProviderMetadataSchema', () => { + it('validates complete OpenID Provider metadata', () => { + const metadata = { + issuer: 'https://auth.example.com', + authorization_endpoint: 'https://auth.example.com/oauth/authorize', + token_endpoint: 'https://auth.example.com/oauth/token', + jwks_uri: 'https://auth.example.com/.well-known/jwks.json', + response_types_supported: ['code'], + subject_types_supported: ['public'], + id_token_signing_alg_values_supported: ['RS256'] + }; + + expect(() => OpenIdProviderMetadataSchema.parse(metadata)).not.toThrow(); + }); + + it('rejects metadata with javascript: in jwks_uri', () => { + const metadata = { + issuer: 'https://auth.example.com', + authorization_endpoint: 'https://auth.example.com/oauth/authorize', + token_endpoint: 'https://auth.example.com/oauth/token', + jwks_uri: 'javascript:alert(1)', + response_types_supported: ['code'], + subject_types_supported: ['public'], + id_token_signing_alg_values_supported: ['RS256'] + }; + + expect(() => OpenIdProviderMetadataSchema.parse(metadata)).toThrow('URL cannot use javascript:, data:, or vbscript: scheme'); + }); +}); + +describe('OAuthClientMetadataSchema', () => { + it('validates client metadata with safe URLs', () => { + const metadata = { + redirect_uris: ['https://app.example.com/callback'], + client_name: 'Test App', + client_uri: 'https://app.example.com' + }; + + expect(() => OAuthClientMetadataSchema.parse(metadata)).not.toThrow(); + }); + + it('rejects client metadata with javascript: redirect URIs', () => { + const metadata = { + redirect_uris: ['javascript:alert(1)'], + client_name: 'Test App' + }; + + expect(() => OAuthClientMetadataSchema.parse(metadata)).toThrow('URL cannot use javascript:, data:, or vbscript: scheme'); + }); +}); diff --git a/packages/core-internal/test/shared/authUtils.test.ts b/packages/core-internal/test/shared/authUtils.test.ts new file mode 100644 index 0000000000..312d1809ef --- /dev/null +++ b/packages/core-internal/test/shared/authUtils.test.ts @@ -0,0 +1,90 @@ +import { checkResourceAllowed, resourceUrlFromServerUrl } from '../../src/shared/authUtils'; + +describe('auth-utils', () => { + describe('resourceUrlFromServerUrl', () => { + it('should remove fragments', () => { + expect(resourceUrlFromServerUrl(new URL('https://example.com/path#fragment')).href).toBe('https://example.com/path'); + expect(resourceUrlFromServerUrl(new URL('https://example.com#fragment')).href).toBe('https://example.com/'); + expect(resourceUrlFromServerUrl(new URL('https://example.com/path?query=1#fragment')).href).toBe( + 'https://example.com/path?query=1' + ); + }); + + it('should return URL unchanged if no fragment', () => { + expect(resourceUrlFromServerUrl(new URL('https://example.com')).href).toBe('https://example.com/'); + expect(resourceUrlFromServerUrl(new URL('https://example.com/path')).href).toBe('https://example.com/path'); + expect(resourceUrlFromServerUrl(new URL('https://example.com/path?query=1')).href).toBe('https://example.com/path?query=1'); + }); + + it('should keep everything else unchanged', () => { + // Case sensitivity preserved + expect(resourceUrlFromServerUrl(new URL('https://EXAMPLE.COM/PATH')).href).toBe('https://example.com/PATH'); + // Ports preserved + expect(resourceUrlFromServerUrl(new URL('https://example.com:443/path')).href).toBe('https://example.com/path'); + expect(resourceUrlFromServerUrl(new URL('https://example.com:8080/path')).href).toBe('https://example.com:8080/path'); + // Query parameters preserved + expect(resourceUrlFromServerUrl(new URL('https://example.com?foo=bar&baz=qux')).href).toBe( + 'https://example.com/?foo=bar&baz=qux' + ); + // Trailing slashes preserved + expect(resourceUrlFromServerUrl(new URL('https://example.com/')).href).toBe('https://example.com/'); + expect(resourceUrlFromServerUrl(new URL('https://example.com/path/')).href).toBe('https://example.com/path/'); + }); + }); + + describe('resourceMatches', () => { + it('should match identical URLs', () => { + expect( + checkResourceAllowed({ requestedResource: 'https://example.com/path', configuredResource: 'https://example.com/path' }) + ).toBe(true); + expect(checkResourceAllowed({ requestedResource: 'https://example.com/', configuredResource: 'https://example.com/' })).toBe( + true + ); + }); + + it('should not match URLs with different paths', () => { + expect( + checkResourceAllowed({ requestedResource: 'https://example.com/path1', configuredResource: 'https://example.com/path2' }) + ).toBe(false); + expect( + checkResourceAllowed({ requestedResource: 'https://example.com/', configuredResource: 'https://example.com/path' }) + ).toBe(false); + }); + + it('should not match URLs with different domains', () => { + expect( + checkResourceAllowed({ requestedResource: 'https://example.com/path', configuredResource: 'https://example.org/path' }) + ).toBe(false); + }); + + it('should not match URLs with different ports', () => { + expect( + checkResourceAllowed({ requestedResource: 'https://example.com:8080/path', configuredResource: 'https://example.com/path' }) + ).toBe(false); + }); + + it('should not match URLs where one path is a sub-path of another', () => { + expect( + checkResourceAllowed({ requestedResource: 'https://example.com/mcpxxxx', configuredResource: 'https://example.com/mcp' }) + ).toBe(false); + expect( + checkResourceAllowed({ + requestedResource: 'https://example.com/folder', + configuredResource: 'https://example.com/folder/subfolder' + }) + ).toBe(false); + expect( + checkResourceAllowed({ requestedResource: 'https://example.com/api/v1', configuredResource: 'https://example.com/api' }) + ).toBe(true); + }); + + it('should handle trailing slashes vs no trailing slashes', () => { + expect( + checkResourceAllowed({ requestedResource: 'https://example.com/mcp/', configuredResource: 'https://example.com/mcp' }) + ).toBe(true); + expect( + checkResourceAllowed({ requestedResource: 'https://example.com/folder', configuredResource: 'https://example.com/folder/' }) + ).toBe(false); + }); + }); +}); diff --git a/packages/core-internal/test/shared/customMethods.test.ts b/packages/core-internal/test/shared/customMethods.test.ts new file mode 100644 index 0000000000..624d6bfdd1 --- /dev/null +++ b/packages/core-internal/test/shared/customMethods.test.ts @@ -0,0 +1,198 @@ +import { describe, expect, it } from 'vitest'; +import { z } from 'zod/v4'; + +import { Protocol } from '../../src/shared/protocol'; +import type { BaseContext, JSONRPCRequest, Result, StandardSchemaV1 } from '../../src/exports/public/index'; +import { ProtocolError } from '../../src/types/index'; +import { SdkErrorCode } from '../../src/errors/sdkErrors'; +import { InMemoryTransport } from '../../src/util/inMemory'; + +class TestProtocol extends Protocol { + protected buildContext(ctx: BaseContext): BaseContext { + return ctx; + } + protected assertCapabilityForMethod(): void {} + protected assertNotificationCapability(): void {} + protected assertRequestHandlerCapability(): void {} +} + +async function pair(): Promise<[TestProtocol, TestProtocol]> { + const [t1, t2] = InMemoryTransport.createLinkedPair(); + const a = new TestProtocol(); + const b = new TestProtocol(); + await a.connect(t1); + await b.connect(t2); + return [a, b]; +} + +describe('Protocol custom-method support', () => { + describe('setRequestHandler 3-arg form', () => { + const SearchParams = z.object({ query: z.string(), limit: z.number().int() }); + const SearchResult = z.object({ items: z.array(z.string()) }); + + it('registers, validates params, and handler receives parsed params', async () => { + const [a, b] = await pair(); + b.setRequestHandler('acme/search', { params: SearchParams, result: SearchResult }, async (params, _ctx) => { + expect(params.query).toBe('hello'); + expect(params.limit).toBe(5); + return { items: [`result for ${params.query}`] }; + }); + + const result = await a.request({ method: 'acme/search', params: { query: 'hello', limit: 5 } }, SearchResult); + expect(result.items).toEqual(['result for hello']); + }); + + it('strips _meta from params before validation', async () => { + const [a, b] = await pair(); + const Strict = z.strictObject({ x: z.number() }); + b.setRequestHandler('acme/strict', { params: Strict }, async params => { + expect(params).toEqual({ x: 1 }); + return {}; + }); + + const result = await a.request({ method: 'acme/strict', params: { x: 1, _meta: { progressToken: 't' } } }, z.object({})); + expect(result).toEqual({}); + }); + + it('rejects invalid params with ProtocolError(InvalidParams)', async () => { + const [a, b] = await pair(); + b.setRequestHandler('acme/search', { params: SearchParams }, async () => ({})); + + await expect(a.request({ method: 'acme/search', params: { query: 'q', limit: 'oops' } }, z.object({}))).rejects.toThrow( + ProtocolError + ); + }); + + it('types handler return from schemas.result', () => { + const p = new TestProtocol(); + p.setRequestHandler('acme/typed', { params: z.object({}), result: SearchResult }, async () => { + return { items: [] }; + }); + // @ts-expect-error wrong return shape when result schema supplied + p.setRequestHandler('acme/typed', { params: z.object({}), result: SearchResult }, async () => ({})); + // No result schema → handler may return any Result + p.setRequestHandler('acme/loose', { params: z.object({}) }, async () => ({}) as Result); + }); + + it('throws TypeError when 2-arg form is used with a non-spec method', () => { + const p = new TestProtocol(); + expect(() => p.setRequestHandler('acme/unknown' as never, () => ({}) as never)).toThrow(TypeError); + }); + + it('routes both 2-arg and 3-arg registration through _wrapHandler', () => { + const seen: string[] = []; + class SpyProtocol extends TestProtocol { + protected override _wrapHandler( + method: string, + handler: (request: JSONRPCRequest, ctx: BaseContext) => Promise + ): (request: JSONRPCRequest, ctx: BaseContext) => Promise { + seen.push(method); + return handler; + } + } + const p = new SpyProtocol(); + p.setRequestHandler('tools/list', () => ({ tools: [] })); + p.setRequestHandler('acme/custom', { params: z.object({}) }, () => ({})); + expect(seen).toContain('tools/list'); + expect(seen).toContain('acme/custom'); + }); + }); + + describe('setNotificationHandler 3-arg form', () => { + it('registers, validates params, handler receives parsed params', async () => { + const [a, b] = await pair(); + const Progress = z.object({ stage: z.string(), pct: z.number() }); + const seen: Array> = []; + b.setNotificationHandler('acme/searchProgress', { params: Progress }, params => { + seen.push(params); + }); + + await a.notification({ method: 'acme/searchProgress', params: { stage: 'fetch', pct: 0.5 } }); + await new Promise(r => setTimeout(r, 0)); + expect(seen).toEqual([{ stage: 'fetch', pct: 0.5 }]); + }); + + it('passes the raw notification (with _meta) as the second handler argument', async () => { + const [a, b] = await pair(); + const Strict = z.strictObject({ stage: z.string() }); + let seenMeta: unknown; + b.setNotificationHandler('acme/searchProgress', { params: Strict }, (params, notification) => { + expect(params).toEqual({ stage: 'fetch' }); + seenMeta = notification.params?._meta; + }); + + await a.notification({ method: 'acme/searchProgress', params: { stage: 'fetch', _meta: { traceId: 't1' } } }); + await new Promise(r => setTimeout(r, 0)); + expect(seenMeta).toEqual({ traceId: 't1' }); + }); + }); + + describe('request() schema overload', () => { + it('validates result against provided schema and types the return', async () => { + const [a, b] = await pair(); + b.setRequestHandler('acme/echo', { params: z.object({ v: z.string() }) }, async params => ({ echoed: params.v })); + + const result = await a.request({ method: 'acme/echo', params: { v: 'x' } }, z.object({ echoed: z.string() })); + expect(result.echoed).toBe('x'); + }); + + it('throws TypeError when 1-arg form is used with a non-spec method', async () => { + const [a] = await pair(); + expect(() => a.request({ method: 'acme/unknown' } as never)).toThrow(TypeError); + }); + + it('rejects with SdkError(InvalidResult) when the response fails the result schema', async () => { + const [a, b] = await pair(); + b.setRequestHandler('acme/bad', { params: z.object({}) }, async () => ({ wrong: 123 })); + + await expect(a.request({ method: 'acme/bad', params: {} }, z.object({ echoed: z.string() }))).rejects.toMatchObject({ + code: SdkErrorCode.InvalidResult + }); + }); + + it('returns the result (and sends no cancellation) if the signal aborts during async result-schema validation', async () => { + const [a, b] = await pair(); + b.setRequestHandler('acme/echo', { params: z.object({}) }, async () => ({ echoed: 'ok' })); + + const cancelled: unknown[] = []; + b.setNotificationHandler('notifications/cancelled', n => { + cancelled.push(n); + }); + + const ac = new AbortController(); + const AsyncEcho: StandardSchemaV1 = { + '~standard': { + version: 1, + vendor: 'test', + validate: value => + new Promise(r => { + ac.abort(); + setTimeout(() => r({ value: value as { echoed: string } }), 0); + }) + } + }; + + const result = await a.request({ method: 'acme/echo', params: {} }, AsyncEcho, { signal: ac.signal }); + expect(result).toEqual({ echoed: 'ok' }); + await new Promise(r => setTimeout(r, 0)); + expect(cancelled).toHaveLength(0); + }); + }); + + describe('ctx.mcpReq.send schema overload', () => { + it('sends a related custom-method request from within a handler', async () => { + const [a, b] = await pair(); + const Pong = z.object({ pong: z.literal(true) }); + + a.setRequestHandler('acme/pong', { params: z.object({}) }, async () => ({ pong: true as const })); + b.setRequestHandler('acme/ping', { params: z.object({}) }, async (_params, ctx) => { + const r = await ctx.mcpReq.send({ method: 'acme/pong', params: {} }, Pong); + expect(r.pong).toBe(true); + return { ok: true }; + }); + + const result = await a.request({ method: 'acme/ping', params: {} }, z.object({ ok: z.boolean() })); + expect(result.ok).toBe(true); + }); + }); +}); diff --git a/packages/core-internal/test/shared/protocol.test.ts b/packages/core-internal/test/shared/protocol.test.ts new file mode 100644 index 0000000000..6f0eda2912 --- /dev/null +++ b/packages/core-internal/test/shared/protocol.test.ts @@ -0,0 +1,912 @@ +import type { MockInstance } from 'vitest'; +import { vi } from 'vitest'; +import * as z from 'zod/v4'; +import type { ZodType } from 'zod/v4'; + +import type { BaseContext } from '../../src/shared/protocol'; +import { mergeCapabilities, Protocol } from '../../src/shared/protocol'; +import type { Transport, TransportSendOptions } from '../../src/shared/transport'; +import type { + ClientCapabilities, + JSONRPCErrorResponse, + JSONRPCMessage, + JSONRPCNotification, + JSONRPCRequest, + JSONRPCResponse, + JSONRPCResultResponse, + Notification, + Request, + RequestId, + Result, + ServerCapabilities +} from '../../src/types/index'; +import { ProtocolError, ProtocolErrorCode } from '../../src/types/index'; +import { SdkError, SdkErrorCode } from '../../src/errors/sdkErrors'; + +// Test Protocol subclass for testing +class TestProtocolImpl extends Protocol { + protected assertCapabilityForMethod(): void {} + protected assertNotificationCapability(): void {} + protected assertRequestHandlerCapability(): void {} + protected buildContext(ctx: BaseContext): BaseContext { + return ctx; + } +} + +function createTestProtocol(): TestProtocolImpl { + return new TestProtocolImpl(); +} + +// Type helper for accessing private/protected Protocol properties in tests +interface TestProtocolInternals { + _responseHandlers: Map void>; +} + +// Mock Transport class +class MockTransport implements Transport { + onclose?: () => void; + onerror?: (error: Error) => void; + onmessage?: (message: unknown) => void; + + async start(): Promise {} + async close(): Promise { + this.onclose?.(); + } + async send(_message: JSONRPCMessage, _options?: TransportSendOptions): Promise {} +} + +/** + * Helper to call the protected _requestWithSchema method from tests that + * use custom method names not present in RequestMethod. + */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function testRequest(proto: Protocol, request: Request, resultSchema: ZodType, options?: any) { + return ( + proto as unknown as { _requestWithSchema: (request: Request, resultSchema: ZodType, options?: unknown) => Promise } + )._requestWithSchema(request, resultSchema, options); +} + +describe('protocol tests', () => { + let protocol: Protocol; + let transport: MockTransport; + let sendSpy: MockInstance; + + beforeEach(() => { + transport = new MockTransport(); + sendSpy = vi.spyOn(transport, 'send'); + protocol = createTestProtocol(); + }); + + test('should throw a timeout error if the request exceeds the timeout', async () => { + await protocol.connect(transport); + const request = { method: 'example', params: {} }; + try { + const mockSchema: ZodType<{ result: string }> = z.object({ + result: z.string() + }); + await testRequest(protocol, request, mockSchema, { + timeout: 0 + }); + } catch (error) { + expect(error).toBeInstanceOf(SdkError); + if (error instanceof SdkError) { + expect(error.code).toBe(SdkErrorCode.RequestTimeout); + } + } + }); + + test('should invoke onclose when the connection is closed', async () => { + const oncloseMock = vi.fn(); + protocol.onclose = oncloseMock; + await protocol.connect(transport); + await transport.close(); + expect(oncloseMock).toHaveBeenCalled(); + }); + + test('should abort in-flight request handlers when the connection is closed', async () => { + await protocol.connect(transport); + + let abortReason: unknown; + let handlerStarted = false; + const handlerDone = new Promise(resolve => { + protocol.setRequestHandler('ping', async (_request, ctx) => { + handlerStarted = true; + await new Promise(resolveInner => { + ctx.mcpReq.signal.addEventListener('abort', () => { + abortReason = ctx.mcpReq.signal.reason; + resolveInner(); + }); + }); + resolve(); + return {}; + }); + }); + + transport.onmessage?.({ jsonrpc: '2.0', id: 1, method: 'ping', params: {} }); + + await vi.waitFor(() => expect(handlerStarted).toBe(true)); + + await transport.close(); + await handlerDone; + + expect(abortReason).toBeInstanceOf(SdkError); + expect((abortReason as SdkError).code).toBe(SdkErrorCode.ConnectionClosed); + }); + + test('should remove abort listener from caller signal when request settles', async () => { + await protocol.connect(transport); + + const controller = new AbortController(); + const addSpy = vi.spyOn(controller.signal, 'addEventListener'); + const removeSpy = vi.spyOn(controller.signal, 'removeEventListener'); + + const mockSchema = z.object({ result: z.string() }); + const reqPromise = testRequest(protocol, { method: 'example', params: {} }, mockSchema, { + signal: controller.signal + }); + + expect(addSpy).toHaveBeenCalledTimes(1); + const listener = addSpy.mock.calls[0]![1]; + + transport.onmessage?.({ jsonrpc: '2.0', id: 0, result: { result: 'ok' } }); + await reqPromise; + + expect(removeSpy).toHaveBeenCalledWith('abort', listener); + }); + + test('should not accumulate abort listeners when reusing a signal across requests', async () => { + await protocol.connect(transport); + + const controller = new AbortController(); + const addSpy = vi.spyOn(controller.signal, 'addEventListener'); + const removeSpy = vi.spyOn(controller.signal, 'removeEventListener'); + + const mockSchema = z.object({ result: z.string() }); + for (let i = 0; i < 5; i++) { + const reqPromise = testRequest(protocol, { method: 'example', params: {} }, mockSchema, { + signal: controller.signal + }); + transport.onmessage?.({ jsonrpc: '2.0', id: i, result: { result: 'ok' } }); + await reqPromise; + } + + expect(addSpy).toHaveBeenCalledTimes(5); + expect(removeSpy).toHaveBeenCalledTimes(5); + }); + + test('should remove abort listener when request rejects', async () => { + await protocol.connect(transport); + + const controller = new AbortController(); + const removeSpy = vi.spyOn(controller.signal, 'removeEventListener'); + + const mockSchema = z.object({ result: z.string() }); + await expect( + testRequest(protocol, { method: 'example', params: {} }, mockSchema, { + signal: controller.signal, + timeout: 0 + }) + ).rejects.toThrow(); + + expect(removeSpy).toHaveBeenCalledWith('abort', expect.any(Function)); + }); + + test('should not overwrite existing hooks when connecting transports', async () => { + const oncloseMock = vi.fn(); + const onerrorMock = vi.fn(); + const onmessageMock = vi.fn(); + transport.onclose = oncloseMock; + transport.onerror = onerrorMock; + transport.onmessage = onmessageMock; + await protocol.connect(transport); + transport.onclose(); + transport.onerror(new Error()); + transport.onmessage(''); + expect(oncloseMock).toHaveBeenCalled(); + expect(onerrorMock).toHaveBeenCalled(); + expect(onmessageMock).toHaveBeenCalled(); + }); + + describe('_meta preservation with onprogress', () => { + test('should preserve existing _meta when adding progressToken', async () => { + await protocol.connect(transport); + const request = { + method: 'example', + params: { + data: 'test', + _meta: { + customField: 'customValue', + anotherField: 123 + } + } + }; + const mockSchema: ZodType<{ result: string }> = z.object({ + result: z.string() + }); + const onProgressMock = vi.fn(); + + // Start request but don't await - we're testing the sent message + void testRequest(protocol, request, mockSchema, { + onprogress: onProgressMock + }).catch(() => { + // May not complete, ignore error + }); + + expect(sendSpy).toHaveBeenCalledWith( + expect.objectContaining({ + method: 'example', + params: { + data: 'test', + _meta: { + customField: 'customValue', + anotherField: 123, + progressToken: expect.any(Number) + } + }, + jsonrpc: '2.0', + id: expect.any(Number) + }), + expect.any(Object) + ); + }); + + test('should create _meta with progressToken when no _meta exists', async () => { + await protocol.connect(transport); + const request = { + method: 'example', + params: { + data: 'test' + } + }; + const mockSchema: ZodType<{ result: string }> = z.object({ + result: z.string() + }); + const onProgressMock = vi.fn(); + + // Start request but don't await - we're testing the sent message + void testRequest(protocol, request, mockSchema, { + onprogress: onProgressMock + }).catch(() => { + // May not complete, ignore error + }); + + expect(sendSpy).toHaveBeenCalledWith( + expect.objectContaining({ + method: 'example', + params: { + data: 'test', + _meta: { + progressToken: expect.any(Number) + } + }, + jsonrpc: '2.0', + id: expect.any(Number) + }), + expect.any(Object) + ); + }); + + test('should not modify _meta when onprogress is not provided', async () => { + await protocol.connect(transport); + const request = { + method: 'example', + params: { + data: 'test', + _meta: { + customField: 'customValue' + } + } + }; + const mockSchema: ZodType<{ result: string }> = z.object({ + result: z.string() + }); + + // Start request but don't await - we're testing the sent message + void testRequest(protocol, request, mockSchema).catch(() => { + // May not complete, ignore error + }); + + expect(sendSpy).toHaveBeenCalledWith( + expect.objectContaining({ + method: 'example', + params: { + data: 'test', + _meta: { + customField: 'customValue' + } + }, + jsonrpc: '2.0', + id: expect.any(Number) + }), + expect.any(Object) + ); + }); + + test('should handle params being undefined with onprogress', async () => { + await protocol.connect(transport); + const request = { + method: 'example' + }; + const mockSchema: ZodType<{ result: string }> = z.object({ + result: z.string() + }); + const onProgressMock = vi.fn(); + + // Start request but don't await - we're testing the sent message + void testRequest(protocol, request, mockSchema, { + onprogress: onProgressMock + }).catch(() => { + // May not complete, ignore error + }); + + expect(sendSpy).toHaveBeenCalledWith( + expect.objectContaining({ + method: 'example', + params: { + _meta: { + progressToken: expect.any(Number) + } + }, + jsonrpc: '2.0', + id: expect.any(Number) + }), + expect.any(Object) + ); + }); + }); + + describe('progress notification timeout behavior', () => { + beforeEach(() => { + vi.useFakeTimers(); + }); + afterEach(() => { + vi.useRealTimers(); + }); + + test('should not reset timeout when resetTimeoutOnProgress is false', async () => { + await protocol.connect(transport); + const request = { method: 'example', params: {} }; + const mockSchema: ZodType<{ result: string }> = z.object({ + result: z.string() + }); + const onProgressMock = vi.fn(); + const requestPromise = testRequest(protocol, request, mockSchema, { + timeout: 1000, + resetTimeoutOnProgress: false, + onprogress: onProgressMock + }); + + vi.advanceTimersByTime(800); + + if (transport.onmessage) { + transport.onmessage({ + jsonrpc: '2.0', + method: 'notifications/progress', + params: { + progressToken: 0, + progress: 50, + total: 100 + } + }); + } + await Promise.resolve(); + + expect(onProgressMock).toHaveBeenCalledWith({ + progress: 50, + total: 100 + }); + + vi.advanceTimersByTime(201); + + await expect(requestPromise).rejects.toThrow('Request timed out'); + }); + + test('should reset timeout when progress notification is received', async () => { + await protocol.connect(transport); + const request = { method: 'example', params: {} }; + const mockSchema: ZodType<{ result: string }> = z.object({ + result: z.string() + }); + const onProgressMock = vi.fn(); + const requestPromise = testRequest(protocol, request, mockSchema, { + timeout: 1000, + resetTimeoutOnProgress: true, + onprogress: onProgressMock + }); + vi.advanceTimersByTime(800); + if (transport.onmessage) { + transport.onmessage({ + jsonrpc: '2.0', + method: 'notifications/progress', + params: { + progressToken: 0, + progress: 50, + total: 100 + } + }); + } + await Promise.resolve(); + expect(onProgressMock).toHaveBeenCalledWith({ + progress: 50, + total: 100 + }); + vi.advanceTimersByTime(800); + if (transport.onmessage) { + transport.onmessage({ + jsonrpc: '2.0', + id: 0, + result: { result: 'success' } + }); + } + await Promise.resolve(); + await expect(requestPromise).resolves.toEqual({ result: 'success' }); + }); + + test('should respect maxTotalTimeout', async () => { + await protocol.connect(transport); + const request = { method: 'example', params: {} }; + const mockSchema: ZodType<{ result: string }> = z.object({ + result: z.string() + }); + const onProgressMock = vi.fn(); + const requestPromise = testRequest(protocol, request, mockSchema, { + timeout: 1000, + maxTotalTimeout: 150, + resetTimeoutOnProgress: true, + onprogress: onProgressMock + }); + + // First progress notification should work + vi.advanceTimersByTime(80); + if (transport.onmessage) { + transport.onmessage({ + jsonrpc: '2.0', + method: 'notifications/progress', + params: { + progressToken: 0, + progress: 50, + total: 100 + } + }); + } + await Promise.resolve(); + expect(onProgressMock).toHaveBeenCalledWith({ + progress: 50, + total: 100 + }); + vi.advanceTimersByTime(80); + if (transport.onmessage) { + transport.onmessage({ + jsonrpc: '2.0', + method: 'notifications/progress', + params: { + progressToken: 0, + progress: 75, + total: 100 + } + }); + } + await expect(requestPromise).rejects.toThrow('Maximum total timeout exceeded'); + expect(onProgressMock).toHaveBeenCalledTimes(1); + }); + + test('should timeout if no progress received within timeout period', async () => { + await protocol.connect(transport); + const request = { method: 'example', params: {} }; + const mockSchema: ZodType<{ result: string }> = z.object({ + result: z.string() + }); + const requestPromise = testRequest(protocol, request, mockSchema, { + timeout: 100, + resetTimeoutOnProgress: true + }); + vi.advanceTimersByTime(101); + await expect(requestPromise).rejects.toThrow('Request timed out'); + }); + + test('should handle multiple progress notifications correctly', async () => { + await protocol.connect(transport); + const request = { method: 'example', params: {} }; + const mockSchema: ZodType<{ result: string }> = z.object({ + result: z.string() + }); + const onProgressMock = vi.fn(); + const requestPromise = testRequest(protocol, request, mockSchema, { + timeout: 1000, + resetTimeoutOnProgress: true, + onprogress: onProgressMock + }); + + // Simulate multiple progress updates + for (let i = 1; i <= 3; i++) { + vi.advanceTimersByTime(800); + if (transport.onmessage) { + transport.onmessage({ + jsonrpc: '2.0', + method: 'notifications/progress', + params: { + progressToken: 0, + progress: i * 25, + total: 100 + } + }); + } + await Promise.resolve(); + expect(onProgressMock).toHaveBeenNthCalledWith(i, { + progress: i * 25, + total: 100 + }); + } + if (transport.onmessage) { + transport.onmessage({ + jsonrpc: '2.0', + id: 0, + result: { result: 'success' } + }); + } + await Promise.resolve(); + await expect(requestPromise).resolves.toEqual({ result: 'success' }); + }); + + test('should handle progress notifications with message field', async () => { + await protocol.connect(transport); + const request = { method: 'example', params: {} }; + const mockSchema: ZodType<{ result: string }> = z.object({ + result: z.string() + }); + const onProgressMock = vi.fn(); + + const requestPromise = testRequest(protocol, request, mockSchema, { + timeout: 1000, + onprogress: onProgressMock + }); + + vi.advanceTimersByTime(200); + + if (transport.onmessage) { + transport.onmessage({ + jsonrpc: '2.0', + method: 'notifications/progress', + params: { + progressToken: 0, + progress: 25, + total: 100, + message: 'Initializing process...' + } + }); + } + await Promise.resolve(); + + expect(onProgressMock).toHaveBeenCalledWith({ + progress: 25, + total: 100, + message: 'Initializing process...' + }); + + vi.advanceTimersByTime(200); + + if (transport.onmessage) { + transport.onmessage({ + jsonrpc: '2.0', + method: 'notifications/progress', + params: { + progressToken: 0, + progress: 75, + total: 100, + message: 'Processing data...' + } + }); + } + await Promise.resolve(); + + expect(onProgressMock).toHaveBeenCalledWith({ + progress: 75, + total: 100, + message: 'Processing data...' + }); + + if (transport.onmessage) { + transport.onmessage({ + jsonrpc: '2.0', + id: 0, + result: { result: 'success' } + }); + } + await Promise.resolve(); + await expect(requestPromise).resolves.toEqual({ result: 'success' }); + }); + }); + + describe('Debounced Notifications', () => { + // We need to flush the microtask queue to test the debouncing logic. + // This helper function does that. + const flushMicrotasks = () => new Promise(resolve => setImmediate(resolve)); + + it('should NOT debounce a notification that has parameters', async () => { + // ARRANGE + protocol = new TestProtocolImpl({ debouncedNotificationMethods: ['test/debounced_with_params'] }); + await protocol.connect(transport); + + // ACT + // These notifications are configured for debouncing but contain params, so they should be sent immediately. + await protocol.notification({ method: 'test/debounced_with_params', params: { data: 1 } }); + await protocol.notification({ method: 'test/debounced_with_params', params: { data: 2 } }); + + // ASSERT + // Both should have been sent immediately to avoid data loss. + expect(sendSpy).toHaveBeenCalledTimes(2); + expect(sendSpy).toHaveBeenCalledWith(expect.objectContaining({ params: { data: 1 } }), undefined); + expect(sendSpy).toHaveBeenCalledWith(expect.objectContaining({ params: { data: 2 } }), undefined); + }); + + it('should NOT debounce a notification that has a relatedRequestId', async () => { + // ARRANGE + protocol = new TestProtocolImpl({ debouncedNotificationMethods: ['test/debounced_with_options'] }); + await protocol.connect(transport); + + // ACT + await protocol.notification({ method: 'test/debounced_with_options' }, { relatedRequestId: 'req-1' }); + await protocol.notification({ method: 'test/debounced_with_options' }, { relatedRequestId: 'req-2' }); + + // ASSERT + expect(sendSpy).toHaveBeenCalledTimes(2); + expect(sendSpy).toHaveBeenCalledWith(expect.any(Object), { relatedRequestId: 'req-1' }); + expect(sendSpy).toHaveBeenCalledWith(expect.any(Object), { relatedRequestId: 'req-2' }); + }); + + it('should clear pending debounced notifications on connection close', async () => { + // ARRANGE + protocol = new TestProtocolImpl({ debouncedNotificationMethods: ['test/debounced'] }); + await protocol.connect(transport); + + // ACT + // Schedule a notification but don't flush the microtask queue. + protocol.notification({ method: 'test/debounced' }); + + // Close the connection. This should clear the pending set. + await protocol.close(); + + // Now, flush the microtask queue. + await flushMicrotasks(); + + // ASSERT + // The send should never have happened because the transport was cleared. + expect(sendSpy).not.toHaveBeenCalled(); + }); + + it('should debounce multiple synchronous calls when params property is omitted', async () => { + // ARRANGE + protocol = new TestProtocolImpl({ debouncedNotificationMethods: ['test/debounced'] }); + await protocol.connect(transport); + + // ACT + // This is the more idiomatic way to write a notification with no params. + protocol.notification({ method: 'test/debounced' }); + protocol.notification({ method: 'test/debounced' }); + protocol.notification({ method: 'test/debounced' }); + + expect(sendSpy).not.toHaveBeenCalled(); + await flushMicrotasks(); + + // ASSERT + expect(sendSpy).toHaveBeenCalledTimes(1); + // The final sent object might not even have the `params` key, which is fine. + // We can check that it was called and that the params are "falsy". + const sentNotification = sendSpy.mock.calls[0]![0]; + expect(sentNotification.method).toBe('test/debounced'); + expect(sentNotification.params).toBeUndefined(); + }); + + it('should debounce calls when params is explicitly undefined', async () => { + // ARRANGE + protocol = new TestProtocolImpl({ debouncedNotificationMethods: ['test/debounced'] }); + await protocol.connect(transport); + + // ACT + protocol.notification({ method: 'test/debounced', params: undefined }); + protocol.notification({ method: 'test/debounced', params: undefined }); + await flushMicrotasks(); + + // ASSERT + expect(sendSpy).toHaveBeenCalledTimes(1); + expect(sendSpy).toHaveBeenCalledWith( + expect.objectContaining({ + method: 'test/debounced', + params: undefined + }), + undefined + ); + }); + + it('should send non-debounced notifications immediately and multiple times', async () => { + // ARRANGE + protocol = new TestProtocolImpl({ debouncedNotificationMethods: ['test/debounced'] }); // Configure for a different method + await protocol.connect(transport); + + // ACT + // Call a non-debounced notification method multiple times. + await protocol.notification({ method: 'test/immediate' }); + await protocol.notification({ method: 'test/immediate' }); + + // ASSERT + // Since this method is not in the debounce list, it should be sent every time. + expect(sendSpy).toHaveBeenCalledTimes(2); + }); + + it('should not debounce any notifications if the option is not provided', async () => { + // ARRANGE + // Use the default protocol from beforeEach, which has no debounce options. + await protocol.connect(transport); + + // ACT + await protocol.notification({ method: 'any/method' }); + await protocol.notification({ method: 'any/method' }); + + // ASSERT + // Without the config, behavior should be immediate sending. + expect(sendSpy).toHaveBeenCalledTimes(2); + }); + + it('should handle sequential batches of debounced notifications correctly', async () => { + // ARRANGE + protocol = new TestProtocolImpl({ debouncedNotificationMethods: ['test/debounced'] }); + await protocol.connect(transport); + + // ACT (Batch 1) + protocol.notification({ method: 'test/debounced' }); + protocol.notification({ method: 'test/debounced' }); + await flushMicrotasks(); + + // ASSERT (Batch 1) + expect(sendSpy).toHaveBeenCalledTimes(1); + + // ACT (Batch 2) + // After the first batch has been sent, a new batch should be possible. + protocol.notification({ method: 'test/debounced' }); + protocol.notification({ method: 'test/debounced' }); + await flushMicrotasks(); + + // ASSERT (Batch 2) + // The total number of sends should now be 2. + expect(sendSpy).toHaveBeenCalledTimes(2); + }); + }); + + describe('notifications/cancelled behavior', () => { + test('should abort request handler when notifications/cancelled is received', async () => { + await protocol.connect(transport); + + // Set up a request handler that checks if it was aborted + let wasAborted = false; + protocol.setRequestHandler('ping', async (_request, ctx) => { + // Simulate a long-running operation + await new Promise(resolve => setTimeout(resolve, 100)); + wasAborted = ctx.mcpReq.signal.aborted; + return {}; + }); + + // Simulate an incoming request + const requestId = 123; + if (transport.onmessage) { + transport.onmessage({ + jsonrpc: '2.0', + id: requestId, + method: 'ping', + params: {} + }); + } + + // Wait a bit for the handler to start + await new Promise(resolve => setTimeout(resolve, 10)); + + // Send cancellation notification + if (transport.onmessage) { + transport.onmessage({ + jsonrpc: '2.0', + method: 'notifications/cancelled', + params: { + requestId: requestId, + reason: 'User cancelled' + } + }); + } + + // Wait for the handler to complete + await new Promise(resolve => setTimeout(resolve, 150)); + + // Verify the request was aborted + expect(wasAborted).toBe(true); + }); + }); +}); + +// (2025-11 experimental test suites removed under SEP-2663; see git history.) +describe('mergeCapabilities', () => { + it('should merge client capabilities', () => { + const base: ClientCapabilities = { + sampling: {}, + roots: { + listChanged: true + } + }; + + const additional: ClientCapabilities = { + experimental: { + feature: { + featureFlag: true + } + }, + elicitation: {}, + roots: { + listChanged: true + } + }; + + const merged = mergeCapabilities(base, additional); + expect(merged).toEqual({ + sampling: {}, + elicitation: {}, + roots: { + listChanged: true + }, + experimental: { + feature: { + featureFlag: true + } + } + }); + }); + + it('should merge server capabilities', () => { + const base: ServerCapabilities = { + logging: {}, + prompts: { + listChanged: true + } + }; + + const additional: ServerCapabilities = { + resources: { + subscribe: true + }, + prompts: { + listChanged: true + } + }; + + const merged = mergeCapabilities(base, additional); + expect(merged).toEqual({ + logging: {}, + prompts: { + listChanged: true + }, + resources: { + subscribe: true + } + }); + }); + + it('should override existing values with additional values', () => { + const base: ServerCapabilities = { + prompts: { + listChanged: false + } + }; + + const additional: ServerCapabilities = { + prompts: { + listChanged: true + } + }; + + const merged = mergeCapabilities(base, additional); + expect(merged.prompts!.listChanged).toBe(true); + }); + + it('should handle empty objects', () => { + const base = {}; + const additional = {}; + const merged = mergeCapabilities(base, additional); + expect(merged).toEqual({}); + }); +}); diff --git a/packages/core-internal/test/shared/protocolTransportHandling.test.ts b/packages/core-internal/test/shared/protocolTransportHandling.test.ts new file mode 100644 index 0000000000..731914265b --- /dev/null +++ b/packages/core-internal/test/shared/protocolTransportHandling.test.ts @@ -0,0 +1,123 @@ +import { beforeEach, describe, expect, test } from 'vitest'; + +import type { BaseContext } from '../../src/shared/protocol'; +import { Protocol } from '../../src/shared/protocol'; +import type { Transport } from '../../src/shared/transport'; +import type { EmptyResult, JSONRPCMessage, Notification, Request, Result } from '../../src/types/index'; + +// Mock Transport class +class MockTransport implements Transport { + id: string; + onclose?: () => void; + onerror?: (error: Error) => void; + onmessage?: (message: unknown) => void; + sentMessages: JSONRPCMessage[] = []; + + constructor(id: string) { + this.id = id; + } + + async start(): Promise {} + + async close(): Promise { + this.onclose?.(); + } + + async send(message: JSONRPCMessage): Promise { + this.sentMessages.push(message); + } +} + +describe('Protocol transport handling bug', () => { + let protocol: Protocol; + let transportA: MockTransport; + let transportB: MockTransport; + + beforeEach(() => { + protocol = new (class extends Protocol { + protected assertCapabilityForMethod(): void {} + protected assertNotificationCapability(): void {} + protected assertRequestHandlerCapability(): void {} + protected buildContext(ctx: BaseContext): BaseContext { + return ctx; + } + })(); + + transportA = new MockTransport('A'); + transportB = new MockTransport('B'); + }); + + test('should send response to the correct transport when multiple clients are connected', async () => { + // Set up a request handler that simulates processing time + let resolveHandler: (value: EmptyResult) => void; + const handlerPromise = new Promise(resolve => { + resolveHandler = resolve; + }); + + protocol.setRequestHandler('ping', async () => handlerPromise); + + // Client A connects and sends a request + await protocol.connect(transportA); + transportA.onmessage?.({ jsonrpc: '2.0', method: 'ping', id: 1 }); + + // While A's request is being processed, client B connects + // This overwrites the transport reference in the protocol + await protocol.connect(transportB); + transportB.onmessage?.({ jsonrpc: '2.0', method: 'ping', id: 2 }); + + // Now complete A's request + resolveHandler!({}); + + // Wait for async operations to complete + await new Promise(resolve => setTimeout(resolve, 10)); + + // Check where the responses went + console.log('Transport A received:', transportA.sentMessages); + console.log('Transport B received:', transportB.sentMessages); + + // Transport A should receive response for request ID 1 + expect(transportA.sentMessages).toHaveLength(1); + expect(transportA.sentMessages[0]).toMatchObject({ jsonrpc: '2.0', id: 1, result: {} }); + + // Transport B should receive response for request ID 2 + expect(transportB.sentMessages).toHaveLength(1); + expect(transportB.sentMessages[0]).toMatchObject({ jsonrpc: '2.0', id: 2, result: {} }); + }); + + test('demonstrates the timing issue with multiple rapid connections', async () => { + const results: { transport: string; response: JSONRPCMessage[] }[] = []; + + // Set up handler with variable delay based on request id + protocol.setRequestHandler('ping', async (_request, ctx) => { + const delay = ctx.mcpReq.id === 1 ? 50 : 10; + await new Promise(resolve => setTimeout(resolve, delay)); + return {}; + }); + + // Rapid succession of connections and requests + await protocol.connect(transportA); + transportA.onmessage?.({ jsonrpc: '2.0', method: 'ping', id: 1 }); + + // Connect B while A is processing + setTimeout(async () => { + await protocol.connect(transportB); + transportB.onmessage?.({ jsonrpc: '2.0', method: 'ping', id: 2 }); + }, 10); + + // Wait for all processing + await new Promise(resolve => setTimeout(resolve, 100)); + + // Collect results + if (transportA.sentMessages.length > 0) { + results.push({ transport: 'A', response: transportA.sentMessages }); + } + if (transportB.sentMessages.length > 0) { + results.push({ transport: 'B', response: transportB.sentMessages }); + } + + console.log('Timing test results:', results); + + expect(transportA.sentMessages).toHaveLength(1); + expect(transportB.sentMessages).toHaveLength(1); + }); +}); diff --git a/packages/core-internal/test/shared/stdio.test.ts b/packages/core-internal/test/shared/stdio.test.ts new file mode 100644 index 0000000000..f8d27a4c1f --- /dev/null +++ b/packages/core-internal/test/shared/stdio.test.ts @@ -0,0 +1,158 @@ +import { ReadBuffer, STDIO_DEFAULT_MAX_BUFFER_SIZE } from '../../src/shared/stdio'; +import type { JSONRPCMessage } from '../../src/types/index'; + +const testMessage: JSONRPCMessage = { + jsonrpc: '2.0', + method: 'foobar' +}; + +test('should have no messages after initialization', () => { + const readBuffer = new ReadBuffer(); + expect(readBuffer.readMessage()).toBeNull(); +}); + +test('should only yield a message after a newline', () => { + const readBuffer = new ReadBuffer(); + + readBuffer.append(Buffer.from(JSON.stringify(testMessage))); + expect(readBuffer.readMessage()).toBeNull(); + + readBuffer.append(Buffer.from('\n')); + expect(readBuffer.readMessage()).toEqual(testMessage); + expect(readBuffer.readMessage()).toBeNull(); +}); + +test('should be reusable after clearing', () => { + const readBuffer = new ReadBuffer(); + + readBuffer.append(Buffer.from('foobar')); + readBuffer.clear(); + expect(readBuffer.readMessage()).toBeNull(); + + readBuffer.append(Buffer.from(JSON.stringify(testMessage))); + readBuffer.append(Buffer.from('\n')); + expect(readBuffer.readMessage()).toEqual(testMessage); +}); + +describe('non-JSON line filtering', () => { + test('should skip empty lines', () => { + const readBuffer = new ReadBuffer(); + readBuffer.append(Buffer.from('\n\n' + JSON.stringify(testMessage) + '\n\n')); + + expect(readBuffer.readMessage()).toEqual(testMessage); + expect(readBuffer.readMessage()).toBeNull(); + }); + + test('should skip non-JSON lines before a valid message', () => { + const readBuffer = new ReadBuffer(); + readBuffer.append(Buffer.from('Debug: Starting server\n' + 'Warning: Something happened\n' + JSON.stringify(testMessage) + '\n')); + + expect(readBuffer.readMessage()).toEqual(testMessage); + expect(readBuffer.readMessage()).toBeNull(); + }); + + test('should skip non-JSON lines interleaved with multiple valid messages', () => { + const readBuffer = new ReadBuffer(); + const message1: JSONRPCMessage = { jsonrpc: '2.0', method: 'method1' }; + const message2: JSONRPCMessage = { jsonrpc: '2.0', method: 'method2' }; + + readBuffer.append( + Buffer.from( + 'Debug line 1\n' + + JSON.stringify(message1) + + '\n' + + 'Debug line 2\n' + + 'Another non-JSON line\n' + + JSON.stringify(message2) + + '\n' + ) + ); + + expect(readBuffer.readMessage()).toEqual(message1); + expect(readBuffer.readMessage()).toEqual(message2); + expect(readBuffer.readMessage()).toBeNull(); + }); + + test('should preserve incomplete JSON at end of buffer until completed', () => { + const readBuffer = new ReadBuffer(); + readBuffer.append(Buffer.from('{"jsonrpc": "2.0", "method": "test"')); + expect(readBuffer.readMessage()).toBeNull(); + + readBuffer.append(Buffer.from('}\n')); + expect(readBuffer.readMessage()).toEqual({ jsonrpc: '2.0', method: 'test' }); + }); + + test('should skip lines with unbalanced braces', () => { + const readBuffer = new ReadBuffer(); + readBuffer.append(Buffer.from('{incomplete\n' + 'incomplete}\n' + JSON.stringify(testMessage) + '\n')); + + expect(readBuffer.readMessage()).toEqual(testMessage); + expect(readBuffer.readMessage()).toBeNull(); + }); + + test('should skip lines that look like JSON but fail to parse', () => { + const readBuffer = new ReadBuffer(); + readBuffer.append(Buffer.from('{invalidJson: true}\n' + JSON.stringify(testMessage) + '\n')); + + expect(readBuffer.readMessage()).toEqual(testMessage); + expect(readBuffer.readMessage()).toBeNull(); + }); + + test('should tolerate leading/trailing whitespace around valid JSON', () => { + const readBuffer = new ReadBuffer(); + const message: JSONRPCMessage = { jsonrpc: '2.0', method: 'test' }; + readBuffer.append(Buffer.from(' ' + JSON.stringify(message) + ' \n')); + + expect(readBuffer.readMessage()).toEqual(message); + }); + + test('should still throw on valid JSON that fails schema validation', () => { + const readBuffer = new ReadBuffer(); + readBuffer.append(Buffer.from('{"not": "a jsonrpc message"}\n')); + + expect(() => readBuffer.readMessage()).toThrow(); + }); +}); + +describe('buffer size limit', () => { + test('should throw when buffer exceeds default max size', () => { + const readBuffer = new ReadBuffer(); + const chunkSize = 1024 * 1024; // 1 MB + const chunk = Buffer.alloc(chunkSize); + const chunksToFill = Math.floor(STDIO_DEFAULT_MAX_BUFFER_SIZE / chunkSize); + for (let i = 0; i < chunksToFill; i++) { + readBuffer.append(chunk); + } + expect(() => readBuffer.append(chunk)).toThrow(/ReadBuffer exceeded maximum size/); + }); + + test('should throw when buffer exceeds custom max size', () => { + const readBuffer = new ReadBuffer({ maxBufferSize: 100 }); + readBuffer.append(Buffer.alloc(50)); + expect(() => readBuffer.append(Buffer.alloc(51))).toThrow(/ReadBuffer exceeded maximum size/); + }); + + test('should clear buffer before throwing on overflow', () => { + const readBuffer = new ReadBuffer({ maxBufferSize: 100 }); + readBuffer.append(Buffer.alloc(50)); + expect(() => readBuffer.append(Buffer.alloc(51))).toThrow(); + + // Buffer should be cleared — can append again + readBuffer.append(Buffer.alloc(50)); + // And read messages normally + expect(readBuffer.readMessage()).toBeNull(); + }); + + test('should allow appending up to exactly the max size', () => { + const readBuffer = new ReadBuffer({ maxBufferSize: 100 }); + // Should not throw — exactly at limit + expect(() => readBuffer.append(Buffer.alloc(100))).not.toThrow(); + }); + + test('should work with no options (backwards compatible)', () => { + const readBuffer = new ReadBuffer(); + // Small append should always work + readBuffer.append(Buffer.from(JSON.stringify({ jsonrpc: '2.0', method: 'ping' }) + '\n')); + expect(readBuffer.readMessage()).not.toBeNull(); + }); +}); diff --git a/packages/core-internal/test/shared/toolNameValidation.test.ts b/packages/core-internal/test/shared/toolNameValidation.test.ts new file mode 100644 index 0000000000..5628b731ca --- /dev/null +++ b/packages/core-internal/test/shared/toolNameValidation.test.ts @@ -0,0 +1,130 @@ +import type { MockInstance } from 'vitest'; +import { vi } from 'vitest'; + +import { issueToolNameWarning, validateAndWarnToolName, validateToolName } from '../../src/shared/toolNameValidation'; + +// Spy on console.warn to capture output +let warnSpy: MockInstance; + +beforeEach(() => { + warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); +}); + +afterEach(() => { + vi.restoreAllMocks(); +}); + +describe('validateToolName', () => { + describe('valid tool names', () => { + test.each` + description | toolName + ${'simple alphanumeric names'} | ${'getUser'} + ${'names with underscores'} | ${'get_user_profile'} + ${'names with dashes'} | ${'user-profile-update'} + ${'names with dots'} | ${'admin.tools.list'} + ${'mixed character names'} | ${'DATA_EXPORT_v2.1'} + ${'single character names'} | ${'a'} + ${'128 character names'} | ${'a'.repeat(128)} + `('should accept $description', ({ toolName }) => { + const result = validateToolName(toolName); + expect(result.isValid).toBe(true); + expect(result.warnings).toHaveLength(0); + }); + }); + + describe('invalid tool names', () => { + test.each` + description | toolName | expectedWarning + ${'empty names'} | ${''} | ${'Tool name cannot be empty'} + ${'names longer than 128 characters'} | ${'a'.repeat(129)} | ${'Tool name exceeds maximum length of 128 characters (current: 129)'} + ${'names with spaces'} | ${'get user profile'} | ${'Tool name contains invalid characters: " "'} + ${'names with commas'} | ${'get,user,profile'} | ${'Tool name contains invalid characters: ","'} + ${'names with forward slashes'} | ${'user/profile/update'} | ${'Tool name contains invalid characters: "/"'} + ${'names with other special chars'} | ${'user@domain.com'} | ${'Tool name contains invalid characters: "@"'} + ${'names with multiple invalid chars'} | ${'user name@domain,com'} | ${'Tool name contains invalid characters: " ", "@", ","'} + ${'names with unicode characters'} | ${'user-ñame'} | ${'Tool name contains invalid characters: "ñ"'} + `('should reject $description', ({ toolName, expectedWarning }) => { + const result = validateToolName(toolName); + expect(result.isValid).toBe(false); + expect(result.warnings).toContain(expectedWarning); + }); + }); + + describe('warnings for potentially problematic patterns', () => { + test.each` + description | toolName | expectedWarning | shouldBeValid + ${'names with spaces'} | ${'get user profile'} | ${'Tool name contains spaces, which may cause parsing issues'} | ${false} + ${'names with commas'} | ${'get,user,profile'} | ${'Tool name contains commas, which may cause parsing issues'} | ${false} + ${'names starting with dash'} | ${'-get-user'} | ${'Tool name starts or ends with a dash, which may cause parsing issues in some contexts'} | ${true} + ${'names ending with dash'} | ${'get-user-'} | ${'Tool name starts or ends with a dash, which may cause parsing issues in some contexts'} | ${true} + ${'names starting with dot'} | ${'.get.user'} | ${'Tool name starts or ends with a dot, which may cause parsing issues in some contexts'} | ${true} + ${'names ending with dot'} | ${'get.user.'} | ${'Tool name starts or ends with a dot, which may cause parsing issues in some contexts'} | ${true} + ${'names with leading and trailing dots'} | ${'.get.user.'} | ${'Tool name starts or ends with a dot, which may cause parsing issues in some contexts'} | ${true} + `('should warn about $description', ({ toolName, expectedWarning, shouldBeValid }) => { + const result = validateToolName(toolName); + expect(result.isValid).toBe(shouldBeValid); + expect(result.warnings).toContain(expectedWarning); + }); + }); +}); + +describe('issueToolNameWarning', () => { + test('should output warnings to console.warn', () => { + const warnings = ['Warning 1', 'Warning 2']; + issueToolNameWarning('test-tool', warnings); + + expect(warnSpy).toHaveBeenCalledTimes(6); // Header + 2 warnings + 3 guidance lines + const calls = warnSpy.mock.calls.map(call => call.join(' ')); + expect(calls[0]).toContain('Tool name validation warning for "test-tool"'); + expect(calls[1]).toContain('- Warning 1'); + expect(calls[2]).toContain('- Warning 2'); + expect(calls[3]).toContain('Tool registration will proceed, but this may cause compatibility issues.'); + expect(calls[4]).toContain('Consider updating the tool name'); + expect(calls[5]).toContain('See SEP: Specify Format for Tool Names'); + }); + + test('should handle empty warnings array', () => { + issueToolNameWarning('test-tool', []); + expect(warnSpy).toHaveBeenCalledTimes(0); + }); +}); + +describe('validateAndWarnToolName', () => { + test.each` + description | toolName | expectedResult | shouldWarn + ${'valid names with warnings'} | ${'-get-user-'} | ${true} | ${true} + ${'completely valid names'} | ${'get-user-profile'} | ${true} | ${false} + ${'invalid names with spaces'} | ${'get user profile'} | ${false} | ${true} + ${'empty names'} | ${''} | ${false} | ${true} + ${'names exceeding length limit'} | ${'a'.repeat(129)} | ${false} | ${true} + `('should handle $description', ({ toolName, expectedResult, shouldWarn }) => { + const result = validateAndWarnToolName(toolName); + expect(result).toBe(expectedResult); + + if (shouldWarn) { + expect(warnSpy).toHaveBeenCalled(); + } else { + expect(warnSpy).not.toHaveBeenCalled(); + } + }); + + test('should include space warning for invalid names with spaces', () => { + validateAndWarnToolName('get user profile'); + const warningCalls = warnSpy.mock.calls.map(call => call.join(' ')); + expect(warningCalls.some(call => call.includes('Tool name contains spaces'))).toBe(true); + }); +}); + +describe('edge cases and robustness', () => { + test.each` + description | toolName | shouldBeValid | expectedWarning + ${'names with only dots'} | ${'...'} | ${true} | ${'Tool name starts or ends with a dot, which may cause parsing issues in some contexts'} + ${'names with only dashes'} | ${'---'} | ${true} | ${'Tool name starts or ends with a dash, which may cause parsing issues in some contexts'} + ${'names with only forward slashes'} | ${'///'} | ${false} | ${'Tool name contains invalid characters: "/"'} + ${'names with mixed valid/invalid chars'} | ${'user@name123'} | ${false} | ${'Tool name contains invalid characters: "@"'} + `('should handle $description', ({ toolName, shouldBeValid, expectedWarning }) => { + const result = validateToolName(toolName); + expect(result.isValid).toBe(shouldBeValid); + expect(result.warnings).toContain(expectedWarning); + }); +}); diff --git a/packages/core-internal/test/shared/traceContextMeta.test.ts b/packages/core-internal/test/shared/traceContextMeta.test.ts new file mode 100644 index 0000000000..312fbca6e3 --- /dev/null +++ b/packages/core-internal/test/shared/traceContextMeta.test.ts @@ -0,0 +1,120 @@ +import { describe, expect, it } from 'vitest'; +import { z } from 'zod/v4'; + +import { Protocol } from '../../src/shared/protocol'; +import type { BaseContext } from '../../src/exports/public/index'; +import { BAGGAGE_META_KEY, TRACEPARENT_META_KEY, TRACESTATE_META_KEY } from '../../src/exports/public/index'; +import { InMemoryTransport } from '../../src/util/inMemory'; + +class TestProtocol extends Protocol { + protected buildContext(ctx: BaseContext): BaseContext { + return ctx; + } + protected assertCapabilityForMethod(): void {} + protected assertNotificationCapability(): void {} + protected assertRequestHandlerCapability(): void {} +} + +async function pair(): Promise<[TestProtocol, TestProtocol]> { + const [t1, t2] = InMemoryTransport.createLinkedPair(); + const a = new TestProtocol(); + const b = new TestProtocol(); + await a.connect(t1); + await b.connect(t2); + return [a, b]; +} + +const TRACEPARENT = '00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01'; +const TRACESTATE = 'vendor1=opaqueValue1,vendor2=opaqueValue2'; +const BAGGAGE = 'userId=alice,serverRegion=us-east-1'; + +describe('SEP-414 trace context `_meta` passthrough', () => { + it('exposes reserved unprefixed key names', () => { + // SEP-414 reserves these exact unprefixed keys as an exception to the + // `_meta` prefix rule; a drifted constant would break interop. + expect(TRACEPARENT_META_KEY).toBe('traceparent'); + expect(TRACESTATE_META_KEY).toBe('tracestate'); + expect(BAGGAGE_META_KEY).toBe('baggage'); + }); + + it('passes request `_meta` trace context through to the server-side handler untouched', async () => { + const [a, b] = await pair(); + let seenMeta: Record | undefined; + b.setRequestHandler('acme/traced', { params: z.object({ v: z.string() }) }, async (params, ctx) => { + seenMeta = ctx.mcpReq._meta; + return { echoed: params.v }; + }); + + await a.request( + { + method: 'acme/traced', + params: { + v: 'x', + _meta: { + [TRACEPARENT_META_KEY]: TRACEPARENT, + [TRACESTATE_META_KEY]: TRACESTATE, + [BAGGAGE_META_KEY]: BAGGAGE + } + } + }, + z.object({ echoed: z.string() }) + ); + + expect(seenMeta).toMatchObject({ + [TRACEPARENT_META_KEY]: TRACEPARENT, + [TRACESTATE_META_KEY]: TRACESTATE, + [BAGGAGE_META_KEY]: BAGGAGE + }); + }); + + it('passes response `_meta` trace context back to the requester untouched', async () => { + const [a, b] = await pair(); + b.setRequestHandler('acme/traced', { params: z.object({}) }, async (_params, ctx) => ({ + ok: true, + _meta: { + // Echo the inbound trace context onto the response envelope. + ...ctx.mcpReq._meta + } + })); + + const result = await a.request( + { + method: 'acme/traced', + params: { + _meta: { + [TRACEPARENT_META_KEY]: TRACEPARENT, + [TRACESTATE_META_KEY]: TRACESTATE, + [BAGGAGE_META_KEY]: BAGGAGE + } + } + }, + z.object({ ok: z.boolean(), _meta: z.record(z.string(), z.unknown()).optional() }) + ); + + expect(result.ok).toBe(true); + expect(result._meta).toMatchObject({ + [TRACEPARENT_META_KEY]: TRACEPARENT, + [TRACESTATE_META_KEY]: TRACESTATE, + [BAGGAGE_META_KEY]: BAGGAGE + }); + }); + + it('passes notification `_meta` trace context through to the handler', async () => { + const [a, b] = await pair(); + let seenMeta: unknown; + b.setNotificationHandler('acme/tracedEvent', { params: z.object({ stage: z.string() }) }, (_params, notification) => { + seenMeta = notification.params?._meta; + }); + + await a.notification({ + method: 'acme/tracedEvent', + params: { + stage: 'fetch', + _meta: { [TRACEPARENT_META_KEY]: TRACEPARENT, [BAGGAGE_META_KEY]: BAGGAGE } + } + }); + await new Promise(r => setTimeout(r, 0)); + + expect(seenMeta).toEqual({ [TRACEPARENT_META_KEY]: TRACEPARENT, [BAGGAGE_META_KEY]: BAGGAGE }); + }); +}); diff --git a/packages/core-internal/test/shared/transport.test.ts b/packages/core-internal/test/shared/transport.test.ts new file mode 100644 index 0000000000..2a17ac0643 --- /dev/null +++ b/packages/core-internal/test/shared/transport.test.ts @@ -0,0 +1,182 @@ +import { createFetchWithInit, type FetchLike, normalizeHeaders } from '../../src/shared/transport'; + +describe('normalizeHeaders', () => { + test('returns empty object for undefined', () => { + expect(normalizeHeaders(undefined)).toEqual({}); + }); + + test('handles Headers instance', () => { + const headers = new Headers({ + 'x-foo': 'bar', + 'content-type': 'application/json' + }); + expect(normalizeHeaders(headers)).toEqual({ + 'x-foo': 'bar', + 'content-type': 'application/json' + }); + }); + + test('handles array of tuples', () => { + const headers: [string, string][] = [ + ['x-foo', 'bar'], + ['x-baz', 'qux'] + ]; + expect(normalizeHeaders(headers)).toEqual({ + 'x-foo': 'bar', + 'x-baz': 'qux' + }); + }); + + test('handles plain object', () => { + const headers = { 'x-foo': 'bar', 'x-baz': 'qux' }; + expect(normalizeHeaders(headers)).toEqual({ + 'x-foo': 'bar', + 'x-baz': 'qux' + }); + }); + + test('returns a shallow copy for plain objects', () => { + const headers = { 'x-foo': 'bar' }; + const result = normalizeHeaders(headers); + expect(result).not.toBe(headers); + expect(result).toEqual(headers); + }); +}); + +describe('createFetchWithInit', () => { + test('returns baseFetch unchanged when no baseInit provided', () => { + const mockFetch: FetchLike = vi.fn(); + const result = createFetchWithInit(mockFetch); + expect(result).toBe(mockFetch); + }); + + test('passes baseInit to fetch when no call init provided', async () => { + const mockFetch: FetchLike = vi.fn(); + const baseInit: RequestInit = { + method: 'POST', + credentials: 'include' + }; + + const wrappedFetch = createFetchWithInit(mockFetch, baseInit); + await wrappedFetch('https://example.com'); + + expect(mockFetch).toHaveBeenCalledWith( + 'https://example.com', + expect.objectContaining({ + method: 'POST', + credentials: 'include' + }) + ); + }); + + test('merges baseInit with call init, call init wins for non-header fields', async () => { + const mockFetch: FetchLike = vi.fn(); + const baseInit: RequestInit = { + method: 'POST', + credentials: 'include' + }; + + const wrappedFetch = createFetchWithInit(mockFetch, baseInit); + await wrappedFetch('https://example.com', { method: 'PUT' }); + + expect(mockFetch).toHaveBeenCalledWith( + 'https://example.com', + expect.objectContaining({ + method: 'PUT', + credentials: 'include' + }) + ); + }); + + test('merges headers from both base and call init', async () => { + const mockFetch: FetchLike = vi.fn(); + const baseInit: RequestInit = { + headers: { 'x-base': 'base-value', 'x-shared': 'base' } + }; + + const wrappedFetch = createFetchWithInit(mockFetch, baseInit); + await wrappedFetch('https://example.com', { + headers: { 'x-call': 'call-value', 'x-shared': 'call' } + }); + + expect(mockFetch).toHaveBeenCalledWith( + 'https://example.com', + expect.objectContaining({ + headers: { + 'x-base': 'base-value', + 'x-call': 'call-value', + 'x-shared': 'call' + } + }) + ); + }); + + test('uses baseInit headers when call init has no headers', async () => { + const mockFetch: FetchLike = vi.fn(); + const baseInit: RequestInit = { + headers: { 'x-base': 'base-value' } + }; + + const wrappedFetch = createFetchWithInit(mockFetch, baseInit); + await wrappedFetch('https://example.com', { method: 'POST' }); + + expect(mockFetch).toHaveBeenCalledWith( + 'https://example.com', + expect.objectContaining({ + method: 'POST', + headers: { 'x-base': 'base-value' } + }) + ); + }); + + test('handles URL object as first argument', async () => { + const mockFetch: FetchLike = vi.fn(); + const baseInit: RequestInit = { method: 'GET' }; + + const wrappedFetch = createFetchWithInit(mockFetch, baseInit); + const url = new URL('https://example.com/path'); + await wrappedFetch(url); + + expect(mockFetch).toHaveBeenCalledWith(url, expect.objectContaining({ method: 'GET' })); + }); + + test('passes all baseInit properties when call init is empty object', async () => { + const mockFetch: FetchLike = vi.fn(); + const baseInit: RequestInit = { + method: 'POST', + credentials: 'include', + headers: { 'x-base': 'value' } + }; + + const wrappedFetch = createFetchWithInit(mockFetch, baseInit); + await wrappedFetch('https://example.com', {}); + + expect(mockFetch).toHaveBeenCalledWith( + 'https://example.com', + expect.objectContaining({ + method: 'POST', + credentials: 'include', + headers: { 'x-base': 'value' } + }) + ); + }); + + test('passes Headers instance through when call init has no headers', async () => { + const mockFetch: FetchLike = vi.fn(); + const baseHeaders = new Headers({ 'x-base': 'value' }); + const baseInit: RequestInit = { + headers: baseHeaders + }; + + const wrappedFetch = createFetchWithInit(mockFetch, baseInit); + await wrappedFetch('https://example.com', { method: 'POST' }); + + expect(mockFetch).toHaveBeenCalledWith( + 'https://example.com', + expect.objectContaining({ + method: 'POST', + headers: baseHeaders + }) + ); + }); +}); diff --git a/packages/core-internal/test/shared/uriTemplate.test.ts b/packages/core-internal/test/shared/uriTemplate.test.ts new file mode 100644 index 0000000000..bfc3237872 --- /dev/null +++ b/packages/core-internal/test/shared/uriTemplate.test.ts @@ -0,0 +1,314 @@ +import { UriTemplate } from '../../src/shared/uriTemplate'; + +describe('UriTemplate', () => { + describe('isTemplate', () => { + it('should return true for strings containing template expressions', () => { + expect(UriTemplate.isTemplate('{foo}')).toBe(true); + expect(UriTemplate.isTemplate('/users/{id}')).toBe(true); + expect(UriTemplate.isTemplate('http://example.com/{path}/{file}')).toBe(true); + expect(UriTemplate.isTemplate('/search{?q,limit}')).toBe(true); + }); + + it('should return false for strings without template expressions', () => { + expect(UriTemplate.isTemplate('')).toBe(false); + expect(UriTemplate.isTemplate('plain string')).toBe(false); + expect(UriTemplate.isTemplate('http://example.com/foo/bar')).toBe(false); + expect(UriTemplate.isTemplate('{}')).toBe(false); // Empty braces don't count + expect(UriTemplate.isTemplate('{ }')).toBe(false); // Just whitespace doesn't count + }); + }); + + describe('simple string expansion', () => { + it('should expand simple string variables', () => { + const template = new UriTemplate('http://example.com/users/{username}'); + expect(template.expand({ username: 'fred' })).toBe('http://example.com/users/fred'); + expect(template.variableNames).toEqual(['username']); + }); + + it('should handle multiple variables', () => { + const template = new UriTemplate('{x,y}'); + expect(template.expand({ x: '1024', y: '768' })).toBe('1024,768'); + expect(template.variableNames).toEqual(['x', 'y']); + }); + + it('should encode reserved characters', () => { + const template = new UriTemplate('{var}'); + expect(template.expand({ var: 'value with spaces' })).toBe('value%20with%20spaces'); + }); + }); + + describe('reserved expansion', () => { + it('should not encode reserved characters with + operator', () => { + const template = new UriTemplate('{+path}/here'); + expect(template.expand({ path: '/foo/bar' })).toBe('/foo/bar/here'); + expect(template.variableNames).toEqual(['path']); + }); + }); + + describe('fragment expansion', () => { + it('should add # prefix and not encode reserved chars', () => { + const template = new UriTemplate('X{#var}'); + expect(template.expand({ var: '/test' })).toBe('X#/test'); + expect(template.variableNames).toEqual(['var']); + }); + }); + + describe('label expansion', () => { + it('should add . prefix', () => { + const template = new UriTemplate('X{.var}'); + expect(template.expand({ var: 'test' })).toBe('X.test'); + expect(template.variableNames).toEqual(['var']); + }); + }); + + describe('path expansion', () => { + it('should add / prefix', () => { + const template = new UriTemplate('X{/var}'); + expect(template.expand({ var: 'test' })).toBe('X/test'); + expect(template.variableNames).toEqual(['var']); + }); + }); + + describe('query expansion', () => { + it('should add ? prefix and name=value format', () => { + const template = new UriTemplate('X{?var}'); + expect(template.expand({ var: 'test' })).toBe('X?var=test'); + expect(template.variableNames).toEqual(['var']); + }); + }); + + describe('form continuation expansion', () => { + it('should add & prefix and name=value format', () => { + const template = new UriTemplate('X{&var}'); + expect(template.expand({ var: 'test' })).toBe('X&var=test'); + expect(template.variableNames).toEqual(['var']); + }); + }); + + describe('matching', () => { + it('should match simple strings and extract variables', () => { + const template = new UriTemplate('http://example.com/users/{username}'); + const match = template.match('http://example.com/users/fred'); + expect(match).toEqual({ username: 'fred' }); + }); + + it('should match multiple variables', () => { + const template = new UriTemplate('/users/{username}/posts/{postId}'); + const match = template.match('/users/fred/posts/123'); + expect(match).toEqual({ username: 'fred', postId: '123' }); + }); + + it('should return null for non-matching URIs', () => { + const template = new UriTemplate('/users/{username}'); + const match = template.match('/posts/123'); + expect(match).toBeNull(); + }); + + it('should handle exploded arrays', () => { + const template = new UriTemplate('{/list*}'); + const match = template.match('/red,green,blue'); + expect(match).toEqual({ list: ['red', 'green', 'blue'] }); + }); + }); + + describe('edge cases', () => { + it('should handle empty variables', () => { + const template = new UriTemplate('{empty}'); + expect(template.expand({})).toBe(''); + expect(template.expand({ empty: '' })).toBe(''); + }); + + it('should handle undefined variables', () => { + const template = new UriTemplate('{a}{b}{c}'); + expect(template.expand({ b: '2' })).toBe('2'); + }); + + it('should handle special characters in variable names', () => { + const template = new UriTemplate('{$var_name}'); + expect(template.expand({ $var_name: 'value' })).toBe('value'); + }); + }); + + describe('complex patterns', () => { + it('should handle nested path segments', () => { + const template = new UriTemplate('/api/{version}/{resource}/{id}'); + expect( + template.expand({ + version: 'v1', + resource: 'users', + id: '123' + }) + ).toBe('/api/v1/users/123'); + expect(template.variableNames).toEqual(['version', 'resource', 'id']); + }); + + it('should handle query parameters with arrays', () => { + const template = new UriTemplate('/search{?tags*}'); + expect( + template.expand({ + tags: ['nodejs', 'typescript', 'testing'] + }) + ).toBe('/search?tags=nodejs,typescript,testing'); + expect(template.variableNames).toEqual(['tags']); + }); + + it('should handle multiple query parameters', () => { + const template = new UriTemplate('/search{?q,page,limit}'); + expect( + template.expand({ + q: 'test', + page: '1', + limit: '10' + }) + ).toBe('/search?q=test&page=1&limit=10'); + expect(template.variableNames).toEqual(['q', 'page', 'limit']); + }); + }); + + describe('matching complex patterns', () => { + it('should match nested path segments', () => { + const template = new UriTemplate('/api/{version}/{resource}/{id}'); + const match = template.match('/api/v1/users/123'); + expect(match).toEqual({ + version: 'v1', + resource: 'users', + id: '123' + }); + expect(template.variableNames).toEqual(['version', 'resource', 'id']); + }); + + it('should match query parameters', () => { + const template = new UriTemplate('/search{?q}'); + const match = template.match('/search?q=test'); + expect(match).toEqual({ q: 'test' }); + expect(template.variableNames).toEqual(['q']); + }); + + it('should match multiple query parameters', () => { + const template = new UriTemplate('/search{?q,page}'); + const match = template.match('/search?q=test&page=1'); + expect(match).toEqual({ q: 'test', page: '1' }); + expect(template.variableNames).toEqual(['q', 'page']); + }); + + it('should handle partial matches correctly', () => { + const template = new UriTemplate('/users/{id}'); + expect(template.match('/users/123/extra')).toBeNull(); + expect(template.match('/users')).toBeNull(); + }); + }); + + describe('security and edge cases', () => { + it('should handle extremely long input strings', () => { + const longString = 'x'.repeat(100_000); + const template = new UriTemplate(`/api/{param}`); + expect(template.expand({ param: longString })).toBe(`/api/${longString}`); + expect(template.match(`/api/${longString}`)).toEqual({ param: longString }); + }); + + it('should handle deeply nested template expressions', () => { + const template = new UriTemplate('{a}{b}{c}{d}{e}{f}{g}{h}{i}{j}'.repeat(1000)); + expect(() => + template.expand({ + a: '1', + b: '2', + c: '3', + d: '4', + e: '5', + f: '6', + g: '7', + h: '8', + i: '9', + j: '0' + }) + ).not.toThrow(); + }); + + it('should handle malformed template expressions', () => { + expect(() => new UriTemplate('{unclosed')).toThrow(); + expect(() => new UriTemplate('{}')).not.toThrow(); + expect(() => new UriTemplate('{,}')).not.toThrow(); + expect(() => new UriTemplate('{a}{')).toThrow(); + }); + + it('should handle pathological regex patterns', () => { + const template = new UriTemplate('/api/{param}'); + // Create a string that could cause catastrophic backtracking + const input = '/api/' + 'a'.repeat(100_000); + expect(() => template.match(input)).not.toThrow(); + }); + + it('should handle invalid UTF-8 sequences', () => { + const template = new UriTemplate('/api/{param}'); + const invalidUtf8 = '���'; + expect(() => template.expand({ param: invalidUtf8 })).not.toThrow(); + expect(() => template.match(`/api/${invalidUtf8}`)).not.toThrow(); + }); + + it('should handle template/URI length mismatches', () => { + const template = new UriTemplate('/api/{param}'); + expect(template.match('/api/')).toBeNull(); + expect(template.match('/api')).toBeNull(); + expect(template.match('/api/value/extra')).toBeNull(); + }); + + it('should handle repeated operators', () => { + const template = new UriTemplate('{?a}{?b}{?c}'); + expect(template.expand({ a: '1', b: '2', c: '3' })).toBe('?a=1&b=2&c=3'); + expect(template.variableNames).toEqual(['a', 'b', 'c']); + }); + + it('should handle overlapping variable names', () => { + const template = new UriTemplate('{var}{vara}'); + expect(template.expand({ var: '1', vara: '2' })).toBe('12'); + expect(template.variableNames).toEqual(['var', 'vara']); + }); + + it('should handle empty segments', () => { + const template = new UriTemplate('///{a}////{b}////'); + expect(template.expand({ a: '1', b: '2' })).toBe('///1////2////'); + expect(template.match('///1////2////')).toEqual({ a: '1', b: '2' }); + expect(template.variableNames).toEqual(['a', 'b']); + }); + + it('should handle maximum template expression limit', () => { + // Create a template with many expressions + const expressions = Array.from({ length: 10_000 }).fill('{param}').join(''); + expect(() => new UriTemplate(expressions)).not.toThrow(); + }); + + it('should handle maximum variable name length', () => { + const longName = 'a'.repeat(10_000); + const template = new UriTemplate(`{${longName}}`); + const vars: Record = { [longName]: 'value' }; + expect(() => template.expand(vars)).not.toThrow(); + }); + + it('should not be vulnerable to ReDoS with exploded path patterns', () => { + // Test for ReDoS vulnerability (CVE-2026-0621) + // See: https://github.com/modelcontextprotocol/typescript-sdk/issues/965 + const template = new UriTemplate('{/id*}'); + const maliciousPayload = '/' + ','.repeat(50); + + const startTime = Date.now(); + template.match(maliciousPayload); + const elapsed = Date.now() - startTime; + + // Should complete in under 100ms, not hang for seconds + expect(elapsed).toBeLessThan(100); + }); + + it('should not be vulnerable to ReDoS with exploded simple patterns', () => { + // Test for ReDoS vulnerability with simple exploded operator + const template = new UriTemplate('{id*}'); + const maliciousPayload = ','.repeat(50); + + const startTime = Date.now(); + template.match(maliciousPayload); + const elapsed = Date.now() - startTime; + + // Should complete in under 100ms, not hang for seconds + expect(elapsed).toBeLessThan(100); + }); + }); +}); diff --git a/packages/core-internal/test/shared/wrapHandler.test.ts b/packages/core-internal/test/shared/wrapHandler.test.ts new file mode 100644 index 0000000000..06a5045ecd --- /dev/null +++ b/packages/core-internal/test/shared/wrapHandler.test.ts @@ -0,0 +1,33 @@ +import { describe, expect, it } from 'vitest'; + +import { Protocol } from '../../src/shared/protocol'; +import type { BaseContext, JSONRPCRequest, Result } from '../../src/exports/public/index'; + +class TestProtocol extends Protocol { + protected buildContext(ctx: BaseContext): BaseContext { + return ctx; + } + protected assertCapabilityForMethod(): void {} + protected assertNotificationCapability(): void {} + protected assertRequestHandlerCapability(): void {} +} + +describe('Protocol._wrapHandler', () => { + it('routes setRequestHandler registration through _wrapHandler', () => { + const seen: string[] = []; + class SpyProtocol extends TestProtocol { + protected override _wrapHandler( + method: string, + handler: (request: JSONRPCRequest, ctx: BaseContext) => Promise + ): (request: JSONRPCRequest, ctx: BaseContext) => Promise { + seen.push(method); + return handler; + } + } + const p = new SpyProtocol(); + seen.length = 0; + p.setRequestHandler('tools/list', () => ({ tools: [] })); + p.setRequestHandler('resources/list', () => ({ resources: [] })); + expect(seen).toEqual(['tools/list', 'resources/list']); + }); +}); diff --git a/packages/core-internal/test/spec.types.2025-11-25.test.ts b/packages/core-internal/test/spec.types.2025-11-25.test.ts new file mode 100644 index 0000000000..ad3fec3f92 --- /dev/null +++ b/packages/core-internal/test/spec.types.2025-11-25.test.ts @@ -0,0 +1,950 @@ +/** + * Compares the SDK's types against the frozen 2025-11-25 release schema + * (spec.types.2025-11-25.ts). The 2026-07-28 comparison lives in + * spec.types.2026-07-28.test.ts. + * + * This contains: + * - Static type checks to verify the Spec's types are compatible with the SDK's types + * (mutually assignable — no type-level workarounds should be needed) + * - Runtime checks to verify each Spec type has a static check + * (note: a few don't have SDK types, see MISSING_SDK_TYPES below) + */ +import fs from 'node:fs'; +import path from 'node:path'; + +import type * as SpecTypes from '../src/types/spec.types.2025-11-25'; +import type * as SDKTypes from '../src/types/index'; + +/* eslint-disable @typescript-eslint/no-unused-vars */ + +// Adds the `jsonrpc` property to a type, to match the on-wire format of notifications. +type WithJSONRPC = T & { jsonrpc: '2.0' }; + +// Adds the `jsonrpc` and `id` properties to a type, to match the on-wire format of requests. +type WithJSONRPCRequest = T & { jsonrpc: '2.0'; id: SDKTypes.RequestId }; + +const sdkTypeChecks = { + RequestParams: (sdk: SDKTypes.RequestParams, spec: SpecTypes.RequestParams) => { + sdk = spec; + spec = sdk; + }, + NotificationParams: (sdk: SDKTypes.NotificationParams, spec: SpecTypes.NotificationParams) => { + sdk = spec; + spec = sdk; + }, + CancelledNotificationParams: (sdk: SDKTypes.CancelledNotificationParams, spec: SpecTypes.CancelledNotificationParams) => { + sdk = spec; + spec = sdk; + }, + InitializeRequestParams: (sdk: SDKTypes.InitializeRequestParams, spec: SpecTypes.InitializeRequestParams) => { + // @ts-expect-error 2025-11-25 types capabilities.experimental values as `object`; the SDK follows the 2026-07-28 schema's JSONObject + sdk = spec; + spec = sdk; + }, + ProgressNotificationParams: (sdk: SDKTypes.ProgressNotificationParams, spec: SpecTypes.ProgressNotificationParams) => { + sdk = spec; + spec = sdk; + }, + ResourceRequestParams: (sdk: SDKTypes.ResourceRequestParams, spec: SpecTypes.ResourceRequestParams) => { + sdk = spec; + spec = sdk; + }, + ReadResourceRequestParams: (sdk: SDKTypes.ReadResourceRequestParams, spec: SpecTypes.ReadResourceRequestParams) => { + sdk = spec; + spec = sdk; + }, + SubscribeRequestParams: (sdk: SDKTypes.SubscribeRequestParams, spec: SpecTypes.SubscribeRequestParams) => { + sdk = spec; + spec = sdk; + }, + UnsubscribeRequestParams: (sdk: SDKTypes.UnsubscribeRequestParams, spec: SpecTypes.UnsubscribeRequestParams) => { + sdk = spec; + spec = sdk; + }, + ResourceUpdatedNotificationParams: ( + sdk: SDKTypes.ResourceUpdatedNotificationParams, + spec: SpecTypes.ResourceUpdatedNotificationParams + ) => { + sdk = spec; + spec = sdk; + }, + GetPromptRequestParams: (sdk: SDKTypes.GetPromptRequestParams, spec: SpecTypes.GetPromptRequestParams) => { + sdk = spec; + spec = sdk; + }, + CallToolRequestParams: (sdk: SDKTypes.CallToolRequestParams, spec: SpecTypes.CallToolRequestParams) => { + sdk = spec; + spec = sdk; + }, + SetLevelRequestParams: (sdk: SDKTypes.SetLevelRequestParams, spec: SpecTypes.SetLevelRequestParams) => { + sdk = spec; + spec = sdk; + }, + LoggingMessageNotificationParams: ( + sdk: SDKTypes.LoggingMessageNotificationParams, + spec: SpecTypes.LoggingMessageNotificationParams + ) => { + sdk = spec; + spec = sdk; + }, + CreateMessageRequestParams: (sdk: SDKTypes.CreateMessageRequestParams, spec: SpecTypes.CreateMessageRequestParams) => { + // @ts-expect-error 2025-11-25 types `metadata` as `object`; the SDK follows the 2026-07-28 schema's JSONObject + sdk = spec; + // @ts-expect-error the SDK's JSONValue-typed tool inputSchema properties are not assignable to 2025-11-25's `object` + spec = sdk; + }, + CompleteRequestParams: (sdk: SDKTypes.CompleteRequestParams, spec: SpecTypes.CompleteRequestParams) => { + sdk = spec; + spec = sdk; + }, + ElicitRequestParams: (sdk: SDKTypes.ElicitRequestParams, spec: SpecTypes.ElicitRequestParams) => { + sdk = spec; + spec = sdk; + }, + ElicitRequestFormParams: (sdk: SDKTypes.ElicitRequestFormParams, spec: SpecTypes.ElicitRequestFormParams) => { + sdk = spec; + spec = sdk; + }, + ElicitRequestURLParams: (sdk: SDKTypes.ElicitRequestURLParams, spec: SpecTypes.ElicitRequestURLParams) => { + sdk = spec; + spec = sdk; + }, + ElicitationCompleteNotification: ( + sdk: WithJSONRPC, + spec: SpecTypes.ElicitationCompleteNotification + ) => { + sdk = spec; + spec = sdk; + }, + PaginatedRequestParams: (sdk: SDKTypes.PaginatedRequestParams, spec: SpecTypes.PaginatedRequestParams) => { + sdk = spec; + spec = sdk; + }, + CancelledNotification: (sdk: WithJSONRPC, spec: SpecTypes.CancelledNotification) => { + sdk = spec; + spec = sdk; + }, + BaseMetadata: (sdk: SDKTypes.BaseMetadata, spec: SpecTypes.BaseMetadata) => { + sdk = spec; + spec = sdk; + }, + Implementation: (sdk: SDKTypes.Implementation, spec: SpecTypes.Implementation) => { + sdk = spec; + spec = sdk; + }, + ProgressNotification: (sdk: WithJSONRPC, spec: SpecTypes.ProgressNotification) => { + sdk = spec; + spec = sdk; + }, + SubscribeRequest: (sdk: WithJSONRPCRequest, spec: SpecTypes.SubscribeRequest) => { + sdk = spec; + spec = sdk; + }, + UnsubscribeRequest: (sdk: WithJSONRPCRequest, spec: SpecTypes.UnsubscribeRequest) => { + sdk = spec; + spec = sdk; + }, + PaginatedRequest: (sdk: WithJSONRPCRequest, spec: SpecTypes.PaginatedRequest) => { + sdk = spec; + spec = sdk; + }, + PaginatedResult: (sdk: SDKTypes.PaginatedResult, spec: SpecTypes.PaginatedResult) => { + sdk = spec; + spec = sdk; + }, + ListRootsRequest: (sdk: WithJSONRPCRequest, spec: SpecTypes.ListRootsRequest) => { + sdk = spec; + spec = sdk; + }, + ListRootsResult: (sdk: SDKTypes.ListRootsResult, spec: SpecTypes.ListRootsResult) => { + sdk = spec; + spec = sdk; + }, + Root: (sdk: SDKTypes.Root, spec: SpecTypes.Root) => { + sdk = spec; + spec = sdk; + }, + ElicitRequest: (sdk: WithJSONRPCRequest, spec: SpecTypes.ElicitRequest) => { + sdk = spec; + spec = sdk; + }, + ElicitResult: (sdk: SDKTypes.ElicitResult, spec: SpecTypes.ElicitResult) => { + sdk = spec; + spec = sdk; + }, + CompleteRequest: (sdk: WithJSONRPCRequest, spec: SpecTypes.CompleteRequest) => { + sdk = spec; + spec = sdk; + }, + CompleteResult: (sdk: SDKTypes.CompleteResult, spec: SpecTypes.CompleteResult) => { + sdk = spec; + spec = sdk; + }, + ProgressToken: (sdk: SDKTypes.ProgressToken, spec: SpecTypes.ProgressToken) => { + sdk = spec; + spec = sdk; + }, + Cursor: (sdk: SDKTypes.Cursor, spec: SpecTypes.Cursor) => { + sdk = spec; + spec = sdk; + }, + Request: (sdk: SDKTypes.Request, spec: SpecTypes.Request) => { + sdk = spec; + spec = sdk; + }, + Result: (sdk: SDKTypes.Result, spec: SpecTypes.Result) => { + sdk = spec; + spec = sdk; + }, + RequestId: (sdk: SDKTypes.RequestId, spec: SpecTypes.RequestId) => { + sdk = spec; + spec = sdk; + }, + JSONRPCRequest: (sdk: SDKTypes.JSONRPCRequest, spec: SpecTypes.JSONRPCRequest) => { + sdk = spec; + spec = sdk; + }, + JSONRPCNotification: (sdk: SDKTypes.JSONRPCNotification, spec: SpecTypes.JSONRPCNotification) => { + sdk = spec; + spec = sdk; + }, + JSONRPCResponse: (sdk: SDKTypes.JSONRPCResponse, spec: SpecTypes.JSONRPCResponse) => { + sdk = spec; + spec = sdk; + }, + EmptyResult: (sdk: SDKTypes.EmptyResult, spec: SpecTypes.EmptyResult) => { + sdk = spec; + spec = sdk; + }, + Notification: (sdk: SDKTypes.Notification, spec: SpecTypes.Notification) => { + sdk = spec; + spec = sdk; + }, + ClientResult: (sdk: SDKTypes.ClientResult, spec: SpecTypes.ClientResult) => { + sdk = spec; + spec = sdk; + }, + ClientNotification: (sdk: WithJSONRPC, spec: SpecTypes.ClientNotification) => { + sdk = spec; + spec = sdk; + }, + ServerResult: (sdk: SDKTypes.ServerResult, spec: SpecTypes.ServerResult) => { + sdk = spec; + spec = sdk; + }, + ResourceTemplateReference: (sdk: SDKTypes.ResourceTemplateReference, spec: SpecTypes.ResourceTemplateReference) => { + sdk = spec; + spec = sdk; + }, + PromptReference: (sdk: SDKTypes.PromptReference, spec: SpecTypes.PromptReference) => { + sdk = spec; + spec = sdk; + }, + ToolAnnotations: (sdk: SDKTypes.ToolAnnotations, spec: SpecTypes.ToolAnnotations) => { + sdk = spec; + spec = sdk; + }, + Tool: (sdk: SDKTypes.Tool, spec: SpecTypes.Tool) => { + // @ts-expect-error 2025-11-25 types inputSchema/outputSchema properties as `object`; the SDK follows the 2026-07-28 schema's JSONValue + sdk = spec; + // @ts-expect-error the SDK's JSONValue-typed inputSchema/outputSchema properties are not assignable to 2025-11-25's `object` + spec = sdk; + }, + ListToolsRequest: (sdk: WithJSONRPCRequest, spec: SpecTypes.ListToolsRequest) => { + sdk = spec; + spec = sdk; + }, + ListToolsResult: (sdk: SDKTypes.ListToolsResult, spec: SpecTypes.ListToolsResult) => { + // @ts-expect-error 2025-11-25 vs 2026-07-28 Tool typing; see the Tool check above + sdk = spec; + // @ts-expect-error 2025-11-25 vs 2026-07-28 Tool typing; see the Tool check above + spec = sdk; + }, + CallToolResult: (sdk: SDKTypes.CallToolResult, spec: SpecTypes.CallToolResult) => { + sdk = spec; + spec = sdk; + }, + CallToolRequest: (sdk: WithJSONRPCRequest, spec: SpecTypes.CallToolRequest) => { + sdk = spec; + spec = sdk; + }, + ToolListChangedNotification: (sdk: WithJSONRPC, spec: SpecTypes.ToolListChangedNotification) => { + sdk = spec; + spec = sdk; + }, + ResourceListChangedNotification: ( + sdk: WithJSONRPC, + spec: SpecTypes.ResourceListChangedNotification + ) => { + sdk = spec; + spec = sdk; + }, + PromptListChangedNotification: ( + sdk: WithJSONRPC, + spec: SpecTypes.PromptListChangedNotification + ) => { + sdk = spec; + spec = sdk; + }, + RootsListChangedNotification: ( + sdk: WithJSONRPC, + spec: SpecTypes.RootsListChangedNotification + ) => { + sdk = spec; + spec = sdk; + }, + ResourceUpdatedNotification: (sdk: WithJSONRPC, spec: SpecTypes.ResourceUpdatedNotification) => { + sdk = spec; + spec = sdk; + }, + SamplingMessage: (sdk: SDKTypes.SamplingMessage, spec: SpecTypes.SamplingMessage) => { + sdk = spec; + spec = sdk; + }, + CreateMessageResult: (sdk: SDKTypes.CreateMessageResultWithTools, spec: SpecTypes.CreateMessageResult) => { + sdk = spec; + spec = sdk; + }, + SetLevelRequest: (sdk: WithJSONRPCRequest, spec: SpecTypes.SetLevelRequest) => { + sdk = spec; + spec = sdk; + }, + PingRequest: (sdk: WithJSONRPCRequest, spec: SpecTypes.PingRequest) => { + sdk = spec; + spec = sdk; + }, + InitializedNotification: (sdk: WithJSONRPC, spec: SpecTypes.InitializedNotification) => { + sdk = spec; + spec = sdk; + }, + ListResourcesRequest: (sdk: WithJSONRPCRequest, spec: SpecTypes.ListResourcesRequest) => { + sdk = spec; + spec = sdk; + }, + ListResourcesResult: (sdk: SDKTypes.ListResourcesResult, spec: SpecTypes.ListResourcesResult) => { + sdk = spec; + spec = sdk; + }, + ListResourceTemplatesRequest: ( + sdk: WithJSONRPCRequest, + spec: SpecTypes.ListResourceTemplatesRequest + ) => { + sdk = spec; + spec = sdk; + }, + ListResourceTemplatesResult: (sdk: SDKTypes.ListResourceTemplatesResult, spec: SpecTypes.ListResourceTemplatesResult) => { + sdk = spec; + spec = sdk; + }, + ReadResourceRequest: (sdk: WithJSONRPCRequest, spec: SpecTypes.ReadResourceRequest) => { + sdk = spec; + spec = sdk; + }, + ReadResourceResult: (sdk: SDKTypes.ReadResourceResult, spec: SpecTypes.ReadResourceResult) => { + sdk = spec; + spec = sdk; + }, + ResourceContents: (sdk: SDKTypes.ResourceContents, spec: SpecTypes.ResourceContents) => { + sdk = spec; + spec = sdk; + }, + TextResourceContents: (sdk: SDKTypes.TextResourceContents, spec: SpecTypes.TextResourceContents) => { + sdk = spec; + spec = sdk; + }, + BlobResourceContents: (sdk: SDKTypes.BlobResourceContents, spec: SpecTypes.BlobResourceContents) => { + sdk = spec; + spec = sdk; + }, + Resource: (sdk: SDKTypes.Resource, spec: SpecTypes.Resource) => { + sdk = spec; + spec = sdk; + }, + ResourceTemplate: (sdk: SDKTypes.ResourceTemplateType, spec: SpecTypes.ResourceTemplate) => { + sdk = spec; + spec = sdk; + }, + PromptArgument: (sdk: SDKTypes.PromptArgument, spec: SpecTypes.PromptArgument) => { + sdk = spec; + spec = sdk; + }, + Prompt: (sdk: SDKTypes.Prompt, spec: SpecTypes.Prompt) => { + sdk = spec; + spec = sdk; + }, + ListPromptsRequest: (sdk: WithJSONRPCRequest, spec: SpecTypes.ListPromptsRequest) => { + sdk = spec; + spec = sdk; + }, + ListPromptsResult: (sdk: SDKTypes.ListPromptsResult, spec: SpecTypes.ListPromptsResult) => { + sdk = spec; + spec = sdk; + }, + GetPromptRequest: (sdk: WithJSONRPCRequest, spec: SpecTypes.GetPromptRequest) => { + sdk = spec; + spec = sdk; + }, + TextContent: (sdk: SDKTypes.TextContent, spec: SpecTypes.TextContent) => { + sdk = spec; + spec = sdk; + }, + ImageContent: (sdk: SDKTypes.ImageContent, spec: SpecTypes.ImageContent) => { + sdk = spec; + spec = sdk; + }, + AudioContent: (sdk: SDKTypes.AudioContent, spec: SpecTypes.AudioContent) => { + sdk = spec; + spec = sdk; + }, + EmbeddedResource: (sdk: SDKTypes.EmbeddedResource, spec: SpecTypes.EmbeddedResource) => { + sdk = spec; + spec = sdk; + }, + ResourceLink: (sdk: SDKTypes.ResourceLink, spec: SpecTypes.ResourceLink) => { + sdk = spec; + spec = sdk; + }, + ContentBlock: (sdk: SDKTypes.ContentBlock, spec: SpecTypes.ContentBlock) => { + sdk = spec; + spec = sdk; + }, + PromptMessage: (sdk: SDKTypes.PromptMessage, spec: SpecTypes.PromptMessage) => { + sdk = spec; + spec = sdk; + }, + GetPromptResult: (sdk: SDKTypes.GetPromptResult, spec: SpecTypes.GetPromptResult) => { + sdk = spec; + spec = sdk; + }, + BooleanSchema: (sdk: SDKTypes.BooleanSchema, spec: SpecTypes.BooleanSchema) => { + sdk = spec; + spec = sdk; + }, + StringSchema: (sdk: SDKTypes.StringSchema, spec: SpecTypes.StringSchema) => { + sdk = spec; + spec = sdk; + }, + NumberSchema: (sdk: SDKTypes.NumberSchema, spec: SpecTypes.NumberSchema) => { + sdk = spec; + spec = sdk; + }, + EnumSchema: (sdk: SDKTypes.EnumSchema, spec: SpecTypes.EnumSchema) => { + sdk = spec; + spec = sdk; + }, + UntitledSingleSelectEnumSchema: (sdk: SDKTypes.UntitledSingleSelectEnumSchema, spec: SpecTypes.UntitledSingleSelectEnumSchema) => { + sdk = spec; + spec = sdk; + }, + TitledSingleSelectEnumSchema: (sdk: SDKTypes.TitledSingleSelectEnumSchema, spec: SpecTypes.TitledSingleSelectEnumSchema) => { + sdk = spec; + spec = sdk; + }, + SingleSelectEnumSchema: (sdk: SDKTypes.SingleSelectEnumSchema, spec: SpecTypes.SingleSelectEnumSchema) => { + sdk = spec; + spec = sdk; + }, + UntitledMultiSelectEnumSchema: (sdk: SDKTypes.UntitledMultiSelectEnumSchema, spec: SpecTypes.UntitledMultiSelectEnumSchema) => { + sdk = spec; + spec = sdk; + }, + TitledMultiSelectEnumSchema: (sdk: SDKTypes.TitledMultiSelectEnumSchema, spec: SpecTypes.TitledMultiSelectEnumSchema) => { + sdk = spec; + spec = sdk; + }, + MultiSelectEnumSchema: (sdk: SDKTypes.MultiSelectEnumSchema, spec: SpecTypes.MultiSelectEnumSchema) => { + sdk = spec; + spec = sdk; + }, + LegacyTitledEnumSchema: (sdk: SDKTypes.LegacyTitledEnumSchema, spec: SpecTypes.LegacyTitledEnumSchema) => { + sdk = spec; + spec = sdk; + }, + PrimitiveSchemaDefinition: (sdk: SDKTypes.PrimitiveSchemaDefinition, spec: SpecTypes.PrimitiveSchemaDefinition) => { + sdk = spec; + spec = sdk; + }, + JSONRPCErrorResponse: (sdk: SDKTypes.JSONRPCErrorResponse, spec: SpecTypes.JSONRPCErrorResponse) => { + sdk = spec; + spec = sdk; + }, + JSONRPCResultResponse: (sdk: SDKTypes.JSONRPCResultResponse, spec: SpecTypes.JSONRPCResultResponse) => { + sdk = spec; + spec = sdk; + }, + JSONRPCMessage: (sdk: SDKTypes.JSONRPCMessage, spec: SpecTypes.JSONRPCMessage) => { + sdk = spec; + spec = sdk; + }, + CreateMessageRequest: (sdk: WithJSONRPCRequest, spec: SpecTypes.CreateMessageRequest) => { + // @ts-expect-error 2025-11-25 vs 2026-07-28 typing of params metadata/tools; see the CreateMessageRequestParams check above + sdk = spec; + // @ts-expect-error 2025-11-25 vs 2026-07-28 typing of params metadata/tools; see the CreateMessageRequestParams check above + spec = sdk; + }, + InitializeRequest: (sdk: WithJSONRPCRequest, spec: SpecTypes.InitializeRequest) => { + // @ts-expect-error 2025-11-25 types capabilities.experimental values as `object`; the SDK follows the 2026-07-28 schema's JSONObject + sdk = spec; + spec = sdk; + }, + InitializeResult: (sdk: SDKTypes.InitializeResult, spec: SpecTypes.InitializeResult) => { + // @ts-expect-error 2025-11-25 types capabilities.experimental values as `object`; the SDK follows the 2026-07-28 schema's JSONObject + sdk = spec; + spec = sdk; + }, + ClientCapabilities: (sdk: SDKTypes.ClientCapabilities, spec: SpecTypes.ClientCapabilities) => { + // @ts-expect-error 2025-11-25 types experimental/sampling/elicitation/tasks blobs as `object`; the SDK follows the 2026-07-28 schema's JSONObject + sdk = spec; + spec = sdk; + }, + ServerCapabilities: (sdk: SDKTypes.ServerCapabilities, spec: SpecTypes.ServerCapabilities) => { + // @ts-expect-error 2025-11-25 types experimental/logging/completions/tasks blobs as `object`; the SDK follows the 2026-07-28 schema's JSONObject + sdk = spec; + spec = sdk; + }, + ClientRequest: (sdk: WithJSONRPCRequest, spec: SpecTypes.ClientRequest) => { + // @ts-expect-error 2025-11-25 types capabilities.experimental values as `object` (via the InitializeRequest member); the SDK follows the 2026-07-28 schema's JSONObject + sdk = spec; + spec = sdk; + }, + ServerRequest: (sdk: WithJSONRPCRequest, spec: SpecTypes.ServerRequest) => { + // @ts-expect-error 2025-11-25 vs 2026-07-28 typing of CreateMessageRequest params; see the CreateMessageRequestParams check above + sdk = spec; + // @ts-expect-error 2025-11-25 vs 2026-07-28 typing of CreateMessageRequest params; see the CreateMessageRequestParams check above + spec = sdk; + }, + LoggingMessageNotification: (sdk: WithJSONRPC, spec: SpecTypes.LoggingMessageNotification) => { + sdk = spec; + spec = sdk; + }, + ServerNotification: (sdk: WithJSONRPC, spec: SpecTypes.ServerNotification) => { + sdk = spec; + spec = sdk; + }, + LoggingLevel: (sdk: SDKTypes.LoggingLevel, spec: SpecTypes.LoggingLevel) => { + sdk = spec; + spec = sdk; + }, + Icon: (sdk: SDKTypes.Icon, spec: SpecTypes.Icon) => { + sdk = spec; + spec = sdk; + }, + Icons: (sdk: SDKTypes.Icons, spec: SpecTypes.Icons) => { + sdk = spec; + spec = sdk; + }, + ModelHint: (sdk: SDKTypes.ModelHint, spec: SpecTypes.ModelHint) => { + sdk = spec; + spec = sdk; + }, + ModelPreferences: (sdk: SDKTypes.ModelPreferences, spec: SpecTypes.ModelPreferences) => { + sdk = spec; + spec = sdk; + }, + ToolChoice: (sdk: SDKTypes.ToolChoice, spec: SpecTypes.ToolChoice) => { + sdk = spec; + spec = sdk; + }, + ToolUseContent: (sdk: SDKTypes.ToolUseContent, spec: SpecTypes.ToolUseContent) => { + sdk = spec; + spec = sdk; + }, + ToolResultContent: (sdk: SDKTypes.ToolResultContent, spec: SpecTypes.ToolResultContent) => { + sdk = spec; + spec = sdk; + }, + SamplingMessageContentBlock: (sdk: SDKTypes.SamplingMessageContentBlock, spec: SpecTypes.SamplingMessageContentBlock) => { + sdk = spec; + spec = sdk; + }, + Annotations: (sdk: SDKTypes.Annotations, spec: SpecTypes.Annotations) => { + sdk = spec; + spec = sdk; + }, + Role: (sdk: SDKTypes.Role, spec: SpecTypes.Role) => { + sdk = spec; + spec = sdk; + }, + ToolExecution: (sdk: SDKTypes.ToolExecution, spec: SpecTypes.ToolExecution) => { + sdk = spec; + spec = sdk; + }, + TaskStatus: (sdk: SDKTypes.TaskStatus, spec: SpecTypes.TaskStatus) => { + sdk = spec; + spec = sdk; + }, + TaskMetadata: (sdk: SDKTypes.TaskMetadata, spec: SpecTypes.TaskMetadata) => { + sdk = spec; + spec = sdk; + }, + RelatedTaskMetadata: (sdk: SDKTypes.RelatedTaskMetadata, spec: SpecTypes.RelatedTaskMetadata) => { + sdk = spec; + spec = sdk; + }, + TaskAugmentedRequestParams: (sdk: SDKTypes.TaskAugmentedRequestParams, spec: SpecTypes.TaskAugmentedRequestParams) => { + sdk = spec; + spec = sdk; + }, + Task: (sdk: SDKTypes.Task, spec: SpecTypes.Task) => { + sdk = spec; + spec = sdk; + }, + CreateTaskResult: (sdk: SDKTypes.CreateTaskResult, spec: SpecTypes.CreateTaskResult) => { + sdk = spec; + spec = sdk; + }, + GetTaskRequest: (sdk: WithJSONRPCRequest, spec: SpecTypes.GetTaskRequest) => { + sdk = spec; + spec = sdk; + }, + GetTaskResult: (sdk: SDKTypes.GetTaskResult, spec: SpecTypes.GetTaskResult) => { + sdk = spec; + spec = sdk; + }, + GetTaskPayloadRequest: (sdk: WithJSONRPCRequest, spec: SpecTypes.GetTaskPayloadRequest) => { + sdk = spec; + spec = sdk; + }, + GetTaskPayloadResult: (sdk: SDKTypes.GetTaskPayloadResult, spec: SpecTypes.GetTaskPayloadResult) => { + sdk = spec; + spec = sdk; + }, + ListTasksRequest: (sdk: WithJSONRPCRequest, spec: SpecTypes.ListTasksRequest) => { + sdk = spec; + spec = sdk; + }, + ListTasksResult: (sdk: SDKTypes.ListTasksResult, spec: SpecTypes.ListTasksResult) => { + sdk = spec; + spec = sdk; + }, + CancelTaskRequest: (sdk: WithJSONRPCRequest, spec: SpecTypes.CancelTaskRequest) => { + sdk = spec; + spec = sdk; + }, + CancelTaskResult: (sdk: SDKTypes.CancelTaskResult, spec: SpecTypes.CancelTaskResult) => { + sdk = spec; + spec = sdk; + }, + TaskStatusNotificationParams: (sdk: SDKTypes.TaskStatusNotificationParams, spec: SpecTypes.TaskStatusNotificationParams) => { + sdk = spec; + spec = sdk; + }, + TaskStatusNotification: (sdk: WithJSONRPC, spec: SpecTypes.TaskStatusNotification) => { + sdk = spec; + spec = sdk; + } +}; + +// --------------------------------------------------------------------------- +// Key-level assertions: verify that each SDK type and its corresponding spec +// type expose exactly the same set of named property keys. This catches cases +// where a Zod schema marks a field as `.optional()` but the spec does not (or +// vice-versa), which the mutual-assignability checks above cannot detect +// because optional fields satisfy structural subtyping in both directions. +// --------------------------------------------------------------------------- + +/** Strip index signatures, keeping only explicitly-named keys. */ +type KnownKeys = keyof { + [K in keyof T as string extends K ? never : number extends K ? never : symbol extends K ? never : K]: T[K]; +}; + +/** + * Assert that A and B have exactly the same set of known (named) keys. + * Resolves to `true` on match; a descriptive error type on mismatch. + */ +type AssertExactKeys< + A, + B, + Extra extends PropertyKey = Exclude, KnownKeys>, + Missing extends PropertyKey = Exclude, KnownKeys> +> = [Extra, Missing] extends [never, never] ? true : { _brand: 'KeyMismatch'; extra: Extra; missing: Missing }; + +/** Constraint: T must resolve to `true`. */ +type Assert = T; + +/** + * Same as {@link AssertExactKeys}, but tolerates the SDK's `resultType` key on + * result shapes: the SDK follows the 2026-07-28 schema's optional `resultType` + * passthrough (absent means "complete"), which is not in released 2025-11-25. + * Every other key still has to match exactly. + */ +type AssertExactKeysWithResultType = AssertExactKeys; + +/* + * Excluded from key-level assertions (21 entries): + * + * Union types — KnownKeys cannot meaningfully enumerate their members (15): + * ClientRequest, ServerRequest, ClientNotification, ServerNotification, + * ClientResult, ServerResult, JSONRPCMessage, JSONRPCResponse, ContentBlock, + * SamplingMessageContentBlock, ElicitRequestParams, PrimitiveSchemaDefinition, + * SingleSelectEnumSchema, MultiSelectEnumSchema, EnumSchema + * + * Primitive type aliases — no object keys to compare (6): + * Role, LoggingLevel, ProgressToken, RequestId, Cursor, TaskStatus + */ + +// -- Simple types (88) -- + +type _K_RequestParams = Assert>; +type _K_NotificationParams = Assert>; +type _K_CancelledNotificationParams = Assert>; +type _K_InitializeRequestParams = Assert>; +type _K_ProgressNotificationParams = Assert>; +type _K_ResourceRequestParams = Assert>; +type _K_ReadResourceRequestParams = Assert>; +type _K_SubscribeRequestParams = Assert>; +type _K_UnsubscribeRequestParams = Assert>; +type _K_ResourceUpdatedNotificationParams = Assert< + AssertExactKeys +>; +type _K_GetPromptRequestParams = Assert>; +type _K_CallToolRequestParams = Assert>; +type _K_SetLevelRequestParams = Assert>; +type _K_LoggingMessageNotificationParams = Assert< + AssertExactKeys +>; +type _K_CreateMessageRequestParams = Assert>; +type _K_CompleteRequestParams = Assert>; +type _K_ElicitRequestFormParams = Assert>; +type _K_ElicitRequestURLParams = Assert>; +type _K_PaginatedRequestParams = Assert>; +type _K_BaseMetadata = Assert>; +type _K_Implementation = Assert>; +type _K_PaginatedResult = Assert>; +type _K_ListRootsResult = Assert>; +type _K_Root = Assert>; +type _K_ElicitResult = Assert>; +type _K_CompleteResult = Assert>; +type _K_Request = Assert>; +type _K_Result = Assert>; +type _K_JSONRPCRequest = Assert>; +type _K_JSONRPCNotification = Assert>; +type _K_EmptyResult = Assert>; +type _K_Notification = Assert>; +type _K_ResourceTemplateReference = Assert>; +// @ts-expect-error Genuine mismatch: SDK PromptReference is missing 'title' from spec +type _K_PromptReference = Assert>; +type _K_ToolAnnotations = Assert>; +type _K_Tool = Assert>; +type _K_ListToolsResult = Assert>; +type _K_CallToolResult = Assert>; +type _K_ListResourcesResult = Assert>; +type _K_ListResourceTemplatesResult = Assert< + AssertExactKeysWithResultType +>; +type _K_ReadResourceResult = Assert>; +type _K_ResourceContents = Assert>; +type _K_TextResourceContents = Assert>; +type _K_BlobResourceContents = Assert>; +type _K_Resource = Assert>; +// @ts-expect-error Genuine mismatch: SDK PromptArgument is missing 'title' from spec +type _K_PromptArgument = Assert>; +type _K_Prompt = Assert>; +type _K_ListPromptsResult = Assert>; +type _K_GetPromptResult = Assert>; +type _K_TextContent = Assert>; +type _K_ImageContent = Assert>; +type _K_AudioContent = Assert>; +type _K_EmbeddedResource = Assert>; +type _K_ResourceLink = Assert>; +type _K_PromptMessage = Assert>; +type _K_BooleanSchema = Assert>; +type _K_StringSchema = Assert>; +type _K_NumberSchema = Assert>; +type _K_UntitledSingleSelectEnumSchema = Assert< + AssertExactKeys +>; +type _K_TitledSingleSelectEnumSchema = Assert< + AssertExactKeys +>; +type _K_UntitledMultiSelectEnumSchema = Assert< + AssertExactKeys +>; +type _K_TitledMultiSelectEnumSchema = Assert>; +type _K_LegacyTitledEnumSchema = Assert>; +type _K_JSONRPCErrorResponse = Assert>; +type _K_JSONRPCResultResponse = Assert>; +type _K_InitializeResult = Assert>; +// @ts-expect-error SDK follows the 2026-07-28 schema's `extensions` capability key; not in released 2025-11-25 +type _K_ClientCapabilities = Assert>; +// @ts-expect-error SDK follows the 2026-07-28 schema's `extensions` capability key; not in released 2025-11-25 +type _K_ServerCapabilities = Assert>; +type _K_SamplingMessage = Assert>; +type _K_Icon = Assert>; +type _K_Icons = Assert>; +type _K_ModelHint = Assert>; +type _K_ModelPreferences = Assert>; +type _K_ToolChoice = Assert>; +type _K_ToolUseContent = Assert>; +type _K_ToolResultContent = Assert>; +type _K_Annotations = Assert>; +type _K_ToolExecution = Assert>; +type _K_TaskMetadata = Assert>; +type _K_RelatedTaskMetadata = Assert>; +type _K_TaskAugmentedRequestParams = Assert>; +type _K_Task = Assert>; +type _K_CreateTaskResult = Assert>; +type _K_GetTaskResult = Assert>; +type _K_GetTaskPayloadResult = Assert>; +type _K_ListTasksResult = Assert>; +type _K_CancelTaskResult = Assert>; +type _K_TaskStatusNotificationParams = Assert< + AssertExactKeys +>; + +// -- WithJSONRPC-wrapped notification types (11) -- +// SDK notification types do not include `jsonrpc` — the spec types do. We wrap +// with WithJSONRPC<> to add the missing field before comparing keys. + +type _K_ElicitationCompleteNotification = Assert< + AssertExactKeys, SpecTypes.ElicitationCompleteNotification> +>; +type _K_CancelledNotification = Assert, SpecTypes.CancelledNotification>>; +type _K_ProgressNotification = Assert, SpecTypes.ProgressNotification>>; +type _K_ToolListChangedNotification = Assert< + AssertExactKeys, SpecTypes.ToolListChangedNotification> +>; +type _K_ResourceListChangedNotification = Assert< + AssertExactKeys, SpecTypes.ResourceListChangedNotification> +>; +type _K_PromptListChangedNotification = Assert< + AssertExactKeys, SpecTypes.PromptListChangedNotification> +>; +type _K_RootsListChangedNotification = Assert< + AssertExactKeys, SpecTypes.RootsListChangedNotification> +>; +type _K_ResourceUpdatedNotification = Assert< + AssertExactKeys, SpecTypes.ResourceUpdatedNotification> +>; +type _K_LoggingMessageNotification = Assert< + AssertExactKeys, SpecTypes.LoggingMessageNotification> +>; +type _K_InitializedNotification = Assert, SpecTypes.InitializedNotification>>; +type _K_TaskStatusNotification = Assert, SpecTypes.TaskStatusNotification>>; + +// -- WithJSONRPCRequest-wrapped request types (21) -- +// SDK request types do not include `jsonrpc` or `id` — the spec types do. We +// wrap with WithJSONRPCRequest<> to add the missing fields before comparing keys. + +type _K_SubscribeRequest = Assert, SpecTypes.SubscribeRequest>>; +type _K_UnsubscribeRequest = Assert, SpecTypes.UnsubscribeRequest>>; +type _K_PaginatedRequest = Assert, SpecTypes.PaginatedRequest>>; +type _K_ListRootsRequest = Assert, SpecTypes.ListRootsRequest>>; +type _K_ElicitRequest = Assert, SpecTypes.ElicitRequest>>; +type _K_CompleteRequest = Assert, SpecTypes.CompleteRequest>>; +type _K_ListToolsRequest = Assert, SpecTypes.ListToolsRequest>>; +type _K_CallToolRequest = Assert, SpecTypes.CallToolRequest>>; +type _K_SetLevelRequest = Assert, SpecTypes.SetLevelRequest>>; +type _K_PingRequest = Assert, SpecTypes.PingRequest>>; +type _K_ListResourcesRequest = Assert, SpecTypes.ListResourcesRequest>>; +type _K_ListResourceTemplatesRequest = Assert< + AssertExactKeys, SpecTypes.ListResourceTemplatesRequest> +>; +type _K_ReadResourceRequest = Assert, SpecTypes.ReadResourceRequest>>; +type _K_ListPromptsRequest = Assert, SpecTypes.ListPromptsRequest>>; +type _K_GetPromptRequest = Assert, SpecTypes.GetPromptRequest>>; +type _K_CreateMessageRequest = Assert, SpecTypes.CreateMessageRequest>>; +type _K_InitializeRequest = Assert, SpecTypes.InitializeRequest>>; +type _K_GetTaskRequest = Assert, SpecTypes.GetTaskRequest>>; +type _K_GetTaskPayloadRequest = Assert< + AssertExactKeys, SpecTypes.GetTaskPayloadRequest> +>; +type _K_ListTasksRequest = Assert, SpecTypes.ListTasksRequest>>; +type _K_CancelTaskRequest = Assert, SpecTypes.CancelTaskRequest>>; + +// -- Name mismatches (2) -- +// SDK exports these under different names than the spec. + +type _K_CreateMessageResult = Assert>; +type _K_ResourceTemplate = Assert>; + +// Types excluded from the key-parity completeness guard: union types and primitive aliases +// that cannot have meaningful AssertExactKeys assertions. +const KEY_PARITY_EXCLUDED = [ + // Union types (15) + 'ClientRequest', + 'ServerRequest', + 'ClientNotification', + 'ServerNotification', + 'ClientResult', + 'ServerResult', + 'JSONRPCMessage', + 'JSONRPCResponse', + 'ContentBlock', + 'SamplingMessageContentBlock', + 'ElicitRequestParams', + 'PrimitiveSchemaDefinition', + 'SingleSelectEnumSchema', + 'MultiSelectEnumSchema', + 'EnumSchema', + // Primitive aliases (6) + 'Role', + 'LoggingLevel', + 'ProgressToken', + 'RequestId', + 'Cursor', + 'TaskStatus' +]; + +// Generated from the frozen 2025-11-25 release schema by `pnpm run fetch:spec-types 2025-11-25`. +const SPEC_TYPES_FILE = path.resolve(__dirname, '../src/types/spec.types.2025-11-25.ts'); +const SDK_TYPES_FILE = path.resolve(__dirname, '../src/types/types.ts'); + +const MISSING_SDK_TYPES = [ + // These are inlined in the SDK: + 'Error', // The inner error object of a JSONRPCError + 'URLElicitationRequiredError' // In the SDK, but with a custom definition +]; + +function extractExportedTypes(source: string): string[] { + const matches = [...source.matchAll(/export\s+(?:interface|class|type)\s+(\w+)\b/g)]; + return matches.map(m => m[1]!); +} + +function extractKeyParityTypes(source: string): string[] { + return [...source.matchAll(/^type _K_(\w+)\s*=/gm)].map(m => m[1]!); +} + +describe('Spec Types (2025-11-25)', () => { + const specTypes = extractExportedTypes(fs.readFileSync(SPEC_TYPES_FILE, 'utf8')); + const sdkTypes = extractExportedTypes(fs.readFileSync(SDK_TYPES_FILE, 'utf8')); + const typesToCheck = specTypes.filter(type => !MISSING_SDK_TYPES.includes(type)); + + it('should define some expected types', () => { + expect(specTypes).toContain('JSONRPCNotification'); + expect(specTypes).toContain('ElicitResult'); + expect(specTypes).toHaveLength(145); + }); + + it('should have up to date list of missing sdk types', () => { + for (const typeName of MISSING_SDK_TYPES) { + expect(sdkTypes).not.toContain(typeName); + } + }); + + it('should have comprehensive compatibility tests', () => { + const missingTests = []; + + for (const typeName of typesToCheck) { + if (!sdkTypeChecks[typeName as keyof typeof sdkTypeChecks]) { + missingTests.push(typeName); + } + } + + expect(missingTests).toHaveLength(0); + }); + + it('should have key-parity assertions for all non-excluded compatibility tests', () => { + const thisSource = fs.readFileSync(__filename, 'utf8'); + const checked = new Set(extractKeyParityTypes(thisSource)); + const excluded = new Set(KEY_PARITY_EXCLUDED); + const missing = Object.keys(sdkTypeChecks).filter(name => !checked.has(name) && !excluded.has(name)); + expect(missing).toHaveLength(0); + }); + + describe('Missing SDK Types', () => { + it.each(MISSING_SDK_TYPES)('%s should not be present in MISSING_SDK_TYPES if it has a compatibility test', type => { + expect(sdkTypeChecks[type as keyof typeof sdkTypeChecks]).toBeUndefined(); + }); + }); +}); diff --git a/packages/core-internal/test/spec.types.2026-07-28.test.ts b/packages/core-internal/test/spec.types.2026-07-28.test.ts new file mode 100644 index 0000000000..7360f2dc79 --- /dev/null +++ b/packages/core-internal/test/spec.types.2026-07-28.test.ts @@ -0,0 +1,550 @@ +/** + * Compares the SDK's types against the upcoming 2026-07-28 schema (spec.types.2026-07-28.ts). + * The frozen-release comparison lives in spec.types.2025-11-25.test.ts. + * + * The SDK does not implement the 2026-07-28 surface yet: every 2026-07-28 type whose shape the SDK + * does not (yet) match is listed in MISSING_SDK_TYPES_2026_07_28 below. Removing a name from + * that list forces a real mutual-assignability check to be added to sdkTypeChecks (the + * completeness tests below fail otherwise) — implementation work burns the list down. + * + * Unlike MISSING_SDK_TYPES in the 2025-11-25 comparison, names in this list may well + * exist in the SDK (e.g. RequestParams) — they are listed because the 2026-07-28 revision changed + * their shape, not necessarily because the SDK lacks them. + */ +import fs from 'node:fs'; +import path from 'node:path'; + +import { + LATEST_PROTOCOL_VERSION, + MISSING_REQUIRED_CLIENT_CAPABILITY, + UNSUPPORTED_PROTOCOL_VERSION +} from '../src/types/spec.types.2026-07-28'; +import type * as SpecTypes from '../src/types/spec.types.2026-07-28'; +import type * as SDKTypes from '../src/types/index'; +import { + CLIENT_CAPABILITIES_META_KEY, + CLIENT_INFO_META_KEY, + LOG_LEVEL_META_KEY, + PROTOCOL_VERSION_META_KEY, + ProtocolErrorCode +} from '../src/types/index'; + +/* eslint-disable @typescript-eslint/no-unused-vars */ + +// Adds the `jsonrpc` property to a type, to match the on-wire format of notifications. +type WithJSONRPC = T & { jsonrpc: '2.0' }; + +// Adds the `jsonrpc` and `id` properties to a type, to match the on-wire format of requests. +type WithJSONRPCRequest = T & { jsonrpc: '2.0'; id: SDKTypes.RequestId }; + +const sdkTypeChecks = { + JSONValue: (sdk: SDKTypes.JSONValue, spec: SpecTypes.JSONValue) => { + sdk = spec; + spec = sdk; + }, + JSONObject: (sdk: SDKTypes.JSONObject, spec: SpecTypes.JSONObject) => { + sdk = spec; + spec = sdk; + }, + JSONArray: (sdk: SDKTypes.JSONArray, spec: SpecTypes.JSONArray) => { + sdk = spec; + spec = sdk; + }, + MetaObject: (sdk: SDKTypes.MetaObject, spec: SpecTypes.MetaObject) => { + sdk = spec; + spec = sdk; + }, + // The SDK models the 2026-07-28 revision's required per-request `_meta` envelope as + // RequestMetaEnvelope (the base request schemas stay lenient; envelope + // requiredness is enforced at dispatch). This check also pins the + // *_META_KEY constants: a drifted key name breaks mutual assignability. + RequestMetaObject: (sdk: SDKTypes.RequestMetaEnvelope, spec: SpecTypes.RequestMetaObject) => { + sdk = spec; + spec = sdk; + }, + ProgressToken: (sdk: SDKTypes.ProgressToken, spec: SpecTypes.ProgressToken) => { + sdk = spec; + spec = sdk; + }, + Cursor: (sdk: SDKTypes.Cursor, spec: SpecTypes.Cursor) => { + sdk = spec; + spec = sdk; + }, + Request: (sdk: SDKTypes.Request, spec: SpecTypes.Request) => { + sdk = spec; + spec = sdk; + }, + NotificationParams: (sdk: SDKTypes.NotificationParams, spec: SpecTypes.NotificationParams) => { + sdk = spec; + spec = sdk; + }, + RequestId: (sdk: SDKTypes.RequestId, spec: SpecTypes.RequestId) => { + sdk = spec; + spec = sdk; + }, + JSONRPCRequest: (sdk: SDKTypes.JSONRPCRequest, spec: SpecTypes.JSONRPCRequest) => { + sdk = spec; + spec = sdk; + }, + JSONRPCNotification: (sdk: WithJSONRPC, spec: SpecTypes.JSONRPCNotification) => { + sdk = spec; + spec = sdk; + }, + JSONRPCErrorResponse: (sdk: SDKTypes.JSONRPCErrorResponse, spec: SpecTypes.JSONRPCErrorResponse) => { + sdk = spec; + spec = sdk; + }, + ParseError: (sdk: SDKTypes.ParseError, spec: SpecTypes.ParseError) => { + sdk = spec; + spec = sdk; + }, + InvalidRequestError: (sdk: SDKTypes.InvalidRequestError, spec: SpecTypes.InvalidRequestError) => { + sdk = spec; + spec = sdk; + }, + MethodNotFoundError: (sdk: SDKTypes.MethodNotFoundError, spec: SpecTypes.MethodNotFoundError) => { + sdk = spec; + spec = sdk; + }, + InvalidParamsError: (sdk: SDKTypes.InvalidParamsError, spec: SpecTypes.InvalidParamsError) => { + sdk = spec; + spec = sdk; + }, + InternalError: (sdk: SDKTypes.InternalError, spec: SpecTypes.InternalError) => { + sdk = spec; + spec = sdk; + }, + CancelledNotificationParams: (sdk: SDKTypes.CancelledNotificationParams, spec: SpecTypes.CancelledNotificationParams) => { + sdk = spec; + spec = sdk; + }, + CancelledNotification: (sdk: WithJSONRPC, spec: SpecTypes.CancelledNotification) => { + sdk = spec; + spec = sdk; + }, + ClientCapabilities: (sdk: SDKTypes.ClientCapabilities, spec: SpecTypes.ClientCapabilities) => { + sdk = spec; + spec = sdk; + }, + ServerCapabilities: (sdk: SDKTypes.ServerCapabilities, spec: SpecTypes.ServerCapabilities) => { + sdk = spec; + spec = sdk; + }, + Icon: (sdk: SDKTypes.Icon, spec: SpecTypes.Icon) => { + sdk = spec; + spec = sdk; + }, + Icons: (sdk: SDKTypes.Icons, spec: SpecTypes.Icons) => { + sdk = spec; + spec = sdk; + }, + BaseMetadata: (sdk: SDKTypes.BaseMetadata, spec: SpecTypes.BaseMetadata) => { + sdk = spec; + spec = sdk; + }, + Implementation: (sdk: SDKTypes.Implementation, spec: SpecTypes.Implementation) => { + sdk = spec; + spec = sdk; + }, + ProgressNotificationParams: (sdk: SDKTypes.ProgressNotificationParams, spec: SpecTypes.ProgressNotificationParams) => { + sdk = spec; + spec = sdk; + }, + ProgressNotification: (sdk: WithJSONRPC, spec: SpecTypes.ProgressNotification) => { + sdk = spec; + spec = sdk; + }, + ResourceListChangedNotification: ( + sdk: WithJSONRPC, + spec: SpecTypes.ResourceListChangedNotification + ) => { + sdk = spec; + spec = sdk; + }, + ResourceUpdatedNotificationParams: ( + sdk: SDKTypes.ResourceUpdatedNotificationParams, + spec: SpecTypes.ResourceUpdatedNotificationParams + ) => { + sdk = spec; + spec = sdk; + }, + ResourceUpdatedNotification: (sdk: WithJSONRPC, spec: SpecTypes.ResourceUpdatedNotification) => { + sdk = spec; + spec = sdk; + }, + Resource: (sdk: SDKTypes.Resource, spec: SpecTypes.Resource) => { + sdk = spec; + spec = sdk; + }, + ResourceTemplate: (sdk: SDKTypes.ResourceTemplateType, spec: SpecTypes.ResourceTemplate) => { + sdk = spec; + spec = sdk; + }, + ResourceContents: (sdk: SDKTypes.ResourceContents, spec: SpecTypes.ResourceContents) => { + sdk = spec; + spec = sdk; + }, + TextResourceContents: (sdk: SDKTypes.TextResourceContents, spec: SpecTypes.TextResourceContents) => { + sdk = spec; + spec = sdk; + }, + BlobResourceContents: (sdk: SDKTypes.BlobResourceContents, spec: SpecTypes.BlobResourceContents) => { + sdk = spec; + spec = sdk; + }, + Prompt: (sdk: SDKTypes.Prompt, spec: SpecTypes.Prompt) => { + sdk = spec; + spec = sdk; + }, + PromptArgument: (sdk: SDKTypes.PromptArgument, spec: SpecTypes.PromptArgument) => { + sdk = spec; + spec = sdk; + }, + Role: (sdk: SDKTypes.Role, spec: SpecTypes.Role) => { + sdk = spec; + spec = sdk; + }, + PromptMessage: (sdk: SDKTypes.PromptMessage, spec: SpecTypes.PromptMessage) => { + sdk = spec; + spec = sdk; + }, + ResourceLink: (sdk: SDKTypes.ResourceLink, spec: SpecTypes.ResourceLink) => { + sdk = spec; + spec = sdk; + }, + EmbeddedResource: (sdk: SDKTypes.EmbeddedResource, spec: SpecTypes.EmbeddedResource) => { + sdk = spec; + spec = sdk; + }, + PromptListChangedNotification: ( + sdk: WithJSONRPC, + spec: SpecTypes.PromptListChangedNotification + ) => { + sdk = spec; + spec = sdk; + }, + ToolListChangedNotification: (sdk: WithJSONRPC, spec: SpecTypes.ToolListChangedNotification) => { + sdk = spec; + spec = sdk; + }, + ToolAnnotations: (sdk: SDKTypes.ToolAnnotations, spec: SpecTypes.ToolAnnotations) => { + sdk = spec; + spec = sdk; + }, + LoggingMessageNotificationParams: ( + sdk: SDKTypes.LoggingMessageNotificationParams, + spec: SpecTypes.LoggingMessageNotificationParams + ) => { + sdk = spec; + spec = sdk; + }, + LoggingMessageNotification: (sdk: WithJSONRPC, spec: SpecTypes.LoggingMessageNotification) => { + sdk = spec; + spec = sdk; + }, + LoggingLevel: (sdk: SDKTypes.LoggingLevel, spec: SpecTypes.LoggingLevel) => { + sdk = spec; + spec = sdk; + }, + ToolChoice: (sdk: SDKTypes.ToolChoice, spec: SpecTypes.ToolChoice) => { + sdk = spec; + spec = sdk; + }, + Annotations: (sdk: SDKTypes.Annotations, spec: SpecTypes.Annotations) => { + sdk = spec; + spec = sdk; + }, + ContentBlock: (sdk: SDKTypes.ContentBlock, spec: SpecTypes.ContentBlock) => { + sdk = spec; + spec = sdk; + }, + TextContent: (sdk: SDKTypes.TextContent, spec: SpecTypes.TextContent) => { + sdk = spec; + spec = sdk; + }, + ImageContent: (sdk: SDKTypes.ImageContent, spec: SpecTypes.ImageContent) => { + sdk = spec; + spec = sdk; + }, + AudioContent: (sdk: SDKTypes.AudioContent, spec: SpecTypes.AudioContent) => { + sdk = spec; + spec = sdk; + }, + ToolUseContent: (sdk: SDKTypes.ToolUseContent, spec: SpecTypes.ToolUseContent) => { + sdk = spec; + spec = sdk; + }, + ModelPreferences: (sdk: SDKTypes.ModelPreferences, spec: SpecTypes.ModelPreferences) => { + sdk = spec; + spec = sdk; + }, + ModelHint: (sdk: SDKTypes.ModelHint, spec: SpecTypes.ModelHint) => { + sdk = spec; + spec = sdk; + }, + ResourceTemplateReference: (sdk: SDKTypes.ResourceTemplateReference, spec: SpecTypes.ResourceTemplateReference) => { + sdk = spec; + spec = sdk; + }, + PromptReference: (sdk: SDKTypes.PromptReference, spec: SpecTypes.PromptReference) => { + sdk = spec; + spec = sdk; + }, + Root: (sdk: SDKTypes.Root, spec: SpecTypes.Root) => { + sdk = spec; + spec = sdk; + }, + ElicitRequestFormParams: (sdk: SDKTypes.ElicitRequestFormParams, spec: SpecTypes.ElicitRequestFormParams) => { + sdk = spec; + spec = sdk; + }, + ElicitRequestURLParams: (sdk: SDKTypes.ElicitRequestURLParams, spec: SpecTypes.ElicitRequestURLParams) => { + sdk = spec; + spec = sdk; + }, + ElicitRequestParams: (sdk: SDKTypes.ElicitRequestParams, spec: SpecTypes.ElicitRequestParams) => { + sdk = spec; + spec = sdk; + }, + ElicitRequest: (sdk: SDKTypes.ElicitRequest, spec: SpecTypes.ElicitRequest) => { + sdk = spec; + spec = sdk; + }, + PrimitiveSchemaDefinition: (sdk: SDKTypes.PrimitiveSchemaDefinition, spec: SpecTypes.PrimitiveSchemaDefinition) => { + sdk = spec; + spec = sdk; + }, + StringSchema: (sdk: SDKTypes.StringSchema, spec: SpecTypes.StringSchema) => { + sdk = spec; + spec = sdk; + }, + NumberSchema: (sdk: SDKTypes.NumberSchema, spec: SpecTypes.NumberSchema) => { + sdk = spec; + spec = sdk; + }, + BooleanSchema: (sdk: SDKTypes.BooleanSchema, spec: SpecTypes.BooleanSchema) => { + sdk = spec; + spec = sdk; + }, + UntitledSingleSelectEnumSchema: (sdk: SDKTypes.UntitledSingleSelectEnumSchema, spec: SpecTypes.UntitledSingleSelectEnumSchema) => { + sdk = spec; + spec = sdk; + }, + TitledSingleSelectEnumSchema: (sdk: SDKTypes.TitledSingleSelectEnumSchema, spec: SpecTypes.TitledSingleSelectEnumSchema) => { + sdk = spec; + spec = sdk; + }, + SingleSelectEnumSchema: (sdk: SDKTypes.SingleSelectEnumSchema, spec: SpecTypes.SingleSelectEnumSchema) => { + sdk = spec; + spec = sdk; + }, + UntitledMultiSelectEnumSchema: (sdk: SDKTypes.UntitledMultiSelectEnumSchema, spec: SpecTypes.UntitledMultiSelectEnumSchema) => { + sdk = spec; + spec = sdk; + }, + TitledMultiSelectEnumSchema: (sdk: SDKTypes.TitledMultiSelectEnumSchema, spec: SpecTypes.TitledMultiSelectEnumSchema) => { + sdk = spec; + spec = sdk; + }, + MultiSelectEnumSchema: (sdk: SDKTypes.MultiSelectEnumSchema, spec: SpecTypes.MultiSelectEnumSchema) => { + sdk = spec; + spec = sdk; + }, + LegacyTitledEnumSchema: (sdk: SDKTypes.LegacyTitledEnumSchema, spec: SpecTypes.LegacyTitledEnumSchema) => { + sdk = spec; + spec = sdk; + }, + EnumSchema: (sdk: SDKTypes.EnumSchema, spec: SpecTypes.EnumSchema) => { + sdk = spec; + spec = sdk; + }, + ElicitationCompleteNotification: ( + sdk: WithJSONRPC, + spec: SpecTypes.ElicitationCompleteNotification + ) => { + sdk = spec; + spec = sdk; + }, + ClientNotification: (sdk: WithJSONRPC, spec: SpecTypes.ClientNotification) => { + sdk = spec; + spec = sdk; + } +}; + +// Generated from the 2026-07-28 schema by `pnpm run fetch:spec-types 2026-07-28 `. +const SPEC_TYPES_FILE = path.resolve(__dirname, '../src/types/spec.types.2026-07-28.ts'); + +/** + * 2026-07-28 spec types the SDK does not match yet. Spec-implementation work for the + * 2026-07-28 release removes entries from this list as the SDK adopts each shape. + */ +const MISSING_SDK_TYPES_2026_07_28 = [ + // Inlined in the SDK (same as the 2025-11-25 comparison): + 'Error', // The inner error object of a JSONRPCError + + // SEP-2575 per-request envelope: 2026-07-28 requests REQUIRE a `_meta` envelope + // (`io.modelcontextprotocol/protocolVersion`, clientInfo, clientCapabilities). The + // envelope itself is modeled by RequestMetaEnvelope (see sdkTypeChecks above); the + // request shapes below stay here because the SDK wire schemas deliberately keep + // `_meta` lenient — the same schemas parse pre-2026 requests (no envelope) and 2026 + // requests, with envelope requiredness enforced per request at dispatch. They burn + // only if the SDK ever models era-specific request types. + 'RequestParams', + 'PaginatedRequestParams', + 'ResourceRequestParams', + 'CallToolRequestParams', + 'CompleteRequestParams', + 'GetPromptRequestParams', + 'ReadResourceRequestParams', + 'CreateMessageRequestParams', + 'PaginatedRequest', + 'CallToolRequest', + 'CompleteRequest', + 'GetPromptRequest', + 'ListPromptsRequest', + 'ListResourceTemplatesRequest', + 'ListResourcesRequest', + 'ListRootsRequest', + 'ListToolsRequest', + 'ReadResourceRequest', + 'CreateMessageRequest', + 'ClientRequest', + + // SEP-2322 (MRTR) → PR for MRTR: 2026-07-28 results carry a required `resultType` + // discriminator. The SDK base result schema carries `resultType` as an optional + // passthrough only (absent means "complete"); per-result modeling lands with MRTR. + 'Result', + 'EmptyResult', + 'PaginatedResult', + 'CallToolResult', + 'CompleteResult', + 'ElicitResult', + 'GetPromptResult', + 'ListPromptsResult', + 'ListResourceTemplatesResult', + 'ListResourcesResult', + 'ListRootsResult', + 'ListToolsResult', + 'ReadResourceResult', + 'CreateMessageResult', + 'ClientResult', + 'ServerResult', + 'ResultType', + + // SEP-2549 cacheable results: `ttlMs`/`cacheScope` caching hints on the list/read + // result shapes → PR for SEP-2549: + 'CacheableResult', + + // Response envelopes embedding the changed Result shape → PR for MRTR: + 'JSONRPCResultResponse', + 'JSONRPCResponse', + 'JSONRPCMessage', + 'CallToolResultResponse', + 'CompleteResultResponse', + 'GetPromptResultResponse', + 'ListPromptsResultResponse', + 'ListResourceTemplatesResultResponse', + 'ListResourcesResultResponse', + 'ListToolsResultResponse', + 'ReadResourceResultResponse', + + // SEP-2575 sessionless discovery: the SDK ships the wire shapes + // (DiscoverRequestSchema / DiscoverResultSchema), but the 2026-07-28 shapes embed the + // required `_meta` envelope (request) and required `resultType` (result → MRTR PR), + // so they do not match yet; DiscoverResultResponse is a response wrapper (→ MRTR PR): + 'DiscoverRequest', + 'DiscoverResult', + 'DiscoverResultResponse', + + // SEP-2567 input requests/responses (new surface) → PR for MRTR: + 'InputRequest', + 'InputRequests', + 'InputRequiredResult', + 'InputResponse', + 'InputResponseRequestParams', + 'InputResponses', + + // 2026-07-28 subscriptions surface (new) → PR for subscriptions/listen: + 'SubscriptionFilter', + 'SubscriptionsAcknowledgedNotification', + 'SubscriptionsAcknowledgedNotificationParams', + 'SubscriptionsListenRequest', + 'SubscriptionsListenRequestParams', + + // New typed protocol errors: the SDK ships -32003/-32004 as ProtocolErrorCode + // entries plus the UnsupportedProtocolVersionError class (errors.ts); the spec's + // per-code error *response envelope* interfaces are not modeled as wire types: + 'MissingRequiredClientCapabilityError', + 'UnsupportedProtocolVersionError', + + // Other shapes changed in the 2026-07-28 schema: sampling content changes (SamplingMessage, + // SamplingMessageContentBlock, ToolResultContent) → backchannel PR; open tool + // input/output schema typing (Tool); loosened Notification.params (Notification); + // server notification union, which gains the subscriptions ack (ServerNotification → + // PR for subscriptions/listen): + 'SamplingMessage', + 'SamplingMessageContentBlock', + 'ToolResultContent', + 'Tool', + 'Notification', + 'ServerNotification' +]; + +function extractExportedTypes(source: string): string[] { + const matches = [...source.matchAll(/export\s+(?:interface|class|type)\s+(\w+)\b/g)]; + return matches.map(m => m[1]!); +} + +describe('Spec Types (2026-07-28)', () => { + const specTypes = extractExportedTypes(fs.readFileSync(SPEC_TYPES_FILE, 'utf8')); + const typesToCheck = specTypes.filter(type => !MISSING_SDK_TYPES_2026_07_28.includes(type)); + + it('pins the 2026-07-28 protocol version and the new error codes', () => { + expect(LATEST_PROTOCOL_VERSION).toBe('2026-07-28'); + expect(MISSING_REQUIRED_CLIENT_CAPABILITY).toBe(-32003); + expect(UNSUPPORTED_PROTOCOL_VERSION).toBe(-32004); + expect(ProtocolErrorCode.MissingRequiredClientCapability).toBe(MISSING_REQUIRED_CLIENT_CAPABILITY); + expect(ProtocolErrorCode.UnsupportedProtocolVersion).toBe(UNSUPPORTED_PROTOCOL_VERSION); + }); + + it('pins the per-request _meta envelope keys to the 2026-07-28 schema', () => { + expect(PROTOCOL_VERSION_META_KEY).toBe('io.modelcontextprotocol/protocolVersion'); + expect(CLIENT_INFO_META_KEY).toBe('io.modelcontextprotocol/clientInfo'); + expect(CLIENT_CAPABILITIES_META_KEY).toBe('io.modelcontextprotocol/clientCapabilities'); + expect(LOG_LEVEL_META_KEY).toBe('io.modelcontextprotocol/logLevel'); + }); + + it('should define some expected types', () => { + expect(specTypes).toContain('DiscoverRequest'); + expect(specTypes).toContain('InputRequiredResult'); + expect(specTypes).toContain('SubscriptionsListenRequest'); + expect(specTypes).toHaveLength(150); + }); + + it('should only allowlist types that exist in the 2026-07-28 schema', () => { + for (const typeName of MISSING_SDK_TYPES_2026_07_28) { + expect(specTypes).toContain(typeName); + } + }); + + it('should have comprehensive compatibility tests', () => { + const missingTests = []; + + for (const typeName of typesToCheck) { + if (!sdkTypeChecks[typeName as keyof typeof sdkTypeChecks]) { + missingTests.push(typeName); + } + } + + expect(missingTests).toHaveLength(0); + }); + + describe('Missing SDK Types', () => { + it.each(MISSING_SDK_TYPES_2026_07_28)( + '%s should not be present in MISSING_SDK_TYPES_2026_07_28 if it has a compatibility test', + type => { + expect(sdkTypeChecks[type as keyof typeof sdkTypeChecks]).toBeUndefined(); + } + ); + }); +}); diff --git a/packages/core-internal/test/types.capabilities.test.ts b/packages/core-internal/test/types.capabilities.test.ts new file mode 100644 index 0000000000..18e7d9745c --- /dev/null +++ b/packages/core-internal/test/types.capabilities.test.ts @@ -0,0 +1,103 @@ +import { ClientCapabilitiesSchema, InitializeRequestParamsSchema } from '../src/types/index'; + +describe('ClientCapabilitiesSchema backwards compatibility', () => { + describe('ElicitationCapabilitySchema preprocessing', () => { + it('should inject form capability when elicitation is an empty object', () => { + const capabilities = { + elicitation: {} + }; + + const result = ClientCapabilitiesSchema.parse(capabilities); + expect(result.elicitation).toBeDefined(); + expect(result.elicitation?.form).toBeDefined(); + expect(result.elicitation?.form).toEqual({}); + expect(result.elicitation?.url).toBeUndefined(); + }); + + it('should preserve form capability configuration including applyDefaults', () => { + const capabilities = { + elicitation: { + form: { + applyDefaults: true + } + } + }; + + const result = ClientCapabilitiesSchema.parse(capabilities); + expect(result.elicitation).toBeDefined(); + expect(result.elicitation?.form).toBeDefined(); + expect(result.elicitation?.form).toEqual({ applyDefaults: true }); + expect(result.elicitation?.url).toBeUndefined(); + }); + + it('should not inject form capability when form is explicitly declared', () => { + const capabilities = { + elicitation: { + form: {} + } + }; + + const result = ClientCapabilitiesSchema.parse(capabilities); + expect(result.elicitation).toBeDefined(); + expect(result.elicitation?.form).toBeDefined(); + expect(result.elicitation?.form).toEqual({}); + expect(result.elicitation?.url).toBeUndefined(); + }); + + it('should not inject form capability when url is explicitly declared', () => { + const capabilities = { + elicitation: { + url: {} + } + }; + + const result = ClientCapabilitiesSchema.parse(capabilities); + expect(result.elicitation).toBeDefined(); + expect(result.elicitation?.url).toBeDefined(); + expect(result.elicitation?.url).toEqual({}); + expect(result.elicitation?.form).toBeUndefined(); + }); + + it('should not inject form capability when both form and url are explicitly declared', () => { + const capabilities = { + elicitation: { + form: {}, + url: {} + } + }; + + const result = ClientCapabilitiesSchema.parse(capabilities); + expect(result.elicitation).toBeDefined(); + expect(result.elicitation?.form).toBeDefined(); + expect(result.elicitation?.url).toBeDefined(); + expect(result.elicitation?.form).toEqual({}); + expect(result.elicitation?.url).toEqual({}); + }); + + it('should not inject form capability when elicitation is undefined', () => { + const capabilities = {}; + + const result = ClientCapabilitiesSchema.parse(capabilities); + // When elicitation is not provided, it should remain undefined + expect(result.elicitation).toBeUndefined(); + }); + + it('should work within InitializeRequestParamsSchema context', () => { + const initializeParams = { + protocolVersion: '2025-11-25', + capabilities: { + elicitation: {} + }, + clientInfo: { + name: 'test client', + version: '1.0' + } + }; + + const result = InitializeRequestParamsSchema.parse(initializeParams); + expect(result.capabilities.elicitation).toBeDefined(); + expect(result.capabilities.elicitation?.form).toBeDefined(); + expect(result.capabilities.elicitation?.form).toEqual({}); + }); + }); +}); diff --git a/packages/core-internal/test/types.test.ts b/packages/core-internal/test/types.test.ts new file mode 100644 index 0000000000..c6c2b5c413 --- /dev/null +++ b/packages/core-internal/test/types.test.ts @@ -0,0 +1,1174 @@ +import { + CallToolRequestSchema, + CallToolResultSchema, + CLIENT_CAPABILITIES_META_KEY, + CLIENT_INFO_META_KEY, + ClientCapabilitiesSchema, + ClientRequestSchema, + CompleteRequestSchema, + ContentBlockSchema, + CreateMessageRequestSchema, + CreateMessageResultSchema, + CreateMessageResultWithToolsSchema, + DiscoverRequestSchema, + DiscoverResultSchema, + ElicitRequestFormParamsSchema, + EmptyResultSchema, + LATEST_PROTOCOL_VERSION, + LOG_LEVEL_META_KEY, + PromptMessageSchema, + PROTOCOL_VERSION_META_KEY, + RequestMetaEnvelopeSchema, + ResourceLinkSchema, + ResultSchema, + SamplingMessageSchema, + SUPPORTED_PROTOCOL_VERSIONS, + ToolChoiceSchema, + ToolResultContentSchema, + ToolSchema, + ToolUseContentSchema +} from '../src/types/index'; + +describe('Types', () => { + test('should have correct latest protocol version', () => { + expect(LATEST_PROTOCOL_VERSION).toBeDefined(); + expect(LATEST_PROTOCOL_VERSION).toBe('2025-11-25'); + }); + test('should have correct supported protocol versions', () => { + expect(SUPPORTED_PROTOCOL_VERSIONS).toBeDefined(); + expect(SUPPORTED_PROTOCOL_VERSIONS).toBeInstanceOf(Array); + expect(SUPPORTED_PROTOCOL_VERSIONS).toContain(LATEST_PROTOCOL_VERSION); + expect(SUPPORTED_PROTOCOL_VERSIONS).toContain('2025-06-18'); + expect(SUPPORTED_PROTOCOL_VERSIONS).toContain('2025-03-26'); + expect(SUPPORTED_PROTOCOL_VERSIONS).toContain('2024-11-05'); + expect(SUPPORTED_PROTOCOL_VERSIONS).toContain('2024-10-07'); + }); + + describe('ResourceLink', () => { + test('should validate a minimal ResourceLink', () => { + const resourceLink = { + type: 'resource_link', + uri: 'file:///path/to/file.txt', + name: 'file.txt' + }; + + const result = ResourceLinkSchema.safeParse(resourceLink); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.type).toBe('resource_link'); + expect(result.data.uri).toBe('file:///path/to/file.txt'); + expect(result.data.name).toBe('file.txt'); + } + }); + + test('should validate a ResourceLink with all optional fields', () => { + const resourceLink = { + type: 'resource_link', + uri: 'https://example.com/resource', + name: 'Example Resource', + title: 'A comprehensive example resource', + description: 'This resource demonstrates all fields', + mimeType: 'text/plain', + _meta: { custom: 'metadata' } + }; + + const result = ResourceLinkSchema.safeParse(resourceLink); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.title).toBe('A comprehensive example resource'); + expect(result.data.description).toBe('This resource demonstrates all fields'); + expect(result.data.mimeType).toBe('text/plain'); + expect(result.data._meta).toEqual({ custom: 'metadata' }); + } + }); + + test('should fail validation for invalid type', () => { + const invalidResourceLink = { + type: 'invalid_type', + uri: 'file:///path/to/file.txt', + name: 'file.txt' + }; + + const result = ResourceLinkSchema.safeParse(invalidResourceLink); + expect(result.success).toBe(false); + }); + + test('should fail validation for missing required fields', () => { + const invalidResourceLink = { + type: 'resource_link', + uri: 'file:///path/to/file.txt' + // missing name + }; + + const result = ResourceLinkSchema.safeParse(invalidResourceLink); + expect(result.success).toBe(false); + }); + }); + + describe('ContentBlock', () => { + test('should validate text content', () => { + const mockDate = new Date().toISOString(); + const textContent = { + type: 'text', + text: 'Hello, world!', + annotations: { + audience: ['user'], + priority: 0.5, + lastModified: mockDate + } + }; + + const result = ContentBlockSchema.safeParse(textContent); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.type).toBe('text'); + expect(result.data.annotations).toEqual({ + audience: ['user'], + priority: 0.5, + lastModified: mockDate + }); + } + }); + + test('should validate image content', () => { + const mockDate = new Date().toISOString(); + const imageContent = { + type: 'image', + data: 'aGVsbG8=', // base64 encoded "hello" + mimeType: 'image/png', + annotations: { + audience: ['user'], + priority: 0.5, + lastModified: mockDate + } + }; + + const result = ContentBlockSchema.safeParse(imageContent); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.type).toBe('image'); + expect(result.data.annotations).toEqual({ + audience: ['user'], + priority: 0.5, + lastModified: mockDate + }); + } + }); + + test('should validate audio content', () => { + const mockDate = new Date().toISOString(); + const audioContent = { + type: 'audio', + data: 'aGVsbG8=', // base64 encoded "hello" + mimeType: 'audio/mp3', + annotations: { + audience: ['user'], + priority: 0.5, + lastModified: mockDate + } + }; + + const result = ContentBlockSchema.safeParse(audioContent); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.type).toBe('audio'); + expect(result.data.annotations).toEqual({ + audience: ['user'], + priority: 0.5, + lastModified: mockDate + }); + } + }); + + test('should validate resource link content', () => { + const mockDate = new Date().toISOString(); + const resourceLink = { + type: 'resource_link', + uri: 'file:///path/to/file.txt', + name: 'file.txt', + mimeType: 'text/plain', + annotations: { + audience: ['user'], + priority: 0.5, + lastModified: mockDate + } + }; + + const result = ContentBlockSchema.safeParse(resourceLink); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.type).toBe('resource_link'); + expect(result.data.annotations).toEqual({ + audience: ['user'], + priority: 0.5, + lastModified: mockDate + }); + } + }); + + test('should validate embedded resource content', () => { + const mockDate = new Date().toISOString(); + const embeddedResource = { + type: 'resource', + resource: { + uri: 'file:///path/to/file.txt', + mimeType: 'text/plain', + text: 'File contents' + }, + annotations: { + audience: ['user'], + priority: 0.5, + lastModified: mockDate + } + }; + + const result = ContentBlockSchema.safeParse(embeddedResource); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.type).toBe('resource'); + expect(result.data.annotations).toEqual({ + audience: ['user'], + priority: 0.5, + lastModified: mockDate + }); + } + }); + }); + + describe('PromptMessage with ContentBlock', () => { + test('should validate prompt message with resource link', () => { + const promptMessage = { + role: 'assistant', + content: { + type: 'resource_link', + uri: 'file:///project/src/main.rs', + name: 'main.rs', + description: 'Primary application entry point', + mimeType: 'text/x-rust' + } + }; + + const result = PromptMessageSchema.safeParse(promptMessage); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.content.type).toBe('resource_link'); + } + }); + }); + + describe('CallToolResult with ContentBlock', () => { + test('should validate tool result with resource links', () => { + const toolResult = { + content: [ + { + type: 'text', + text: 'Found the following files:' + }, + { + type: 'resource_link', + uri: 'file:///project/src/main.rs', + name: 'main.rs', + description: 'Primary application entry point', + mimeType: 'text/x-rust' + }, + { + type: 'resource_link', + uri: 'file:///project/src/lib.rs', + name: 'lib.rs', + description: 'Library exports', + mimeType: 'text/x-rust' + } + ] + }; + + const result = CallToolResultSchema.safeParse(toolResult); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.content).toHaveLength(3); + expect(result.data.content[0]?.type).toBe('text'); + expect(result.data.content[1]?.type).toBe('resource_link'); + expect(result.data.content[2]?.type).toBe('resource_link'); + } + }); + + test('should validate empty content array with default', () => { + const toolResult = {}; + + const result = CallToolResultSchema.safeParse(toolResult); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.content).toEqual([]); + } + }); + }); + + describe('CompleteRequest', () => { + test('should validate a CompleteRequest without resolved field', () => { + const request = { + method: 'completion/complete', + params: { + ref: { type: 'ref/prompt', name: 'greeting' }, + argument: { name: 'name', value: 'A' } + } + }; + + const result = CompleteRequestSchema.safeParse(request); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.method).toBe('completion/complete'); + expect(result.data.params.ref.type).toBe('ref/prompt'); + expect(result.data.params.context).toBeUndefined(); + } + }); + + test('should validate a CompleteRequest with resolved field', () => { + const request = { + method: 'completion/complete', + params: { + ref: { type: 'ref/resource', uri: 'github://repos/{owner}/{repo}' }, + argument: { name: 'repo', value: 't' }, + context: { + arguments: { + '{owner}': 'microsoft' + } + } + } + }; + + const result = CompleteRequestSchema.safeParse(request); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.params.context?.arguments).toEqual({ + '{owner}': 'microsoft' + }); + } + }); + + test('should validate a CompleteRequest with empty resolved field', () => { + const request = { + method: 'completion/complete', + params: { + ref: { type: 'ref/prompt', name: 'test' }, + argument: { name: 'arg', value: '' }, + context: { + arguments: {} + } + } + }; + + const result = CompleteRequestSchema.safeParse(request); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.params.context?.arguments).toEqual({}); + } + }); + + test('should validate a CompleteRequest with multiple resolved variables', () => { + const request = { + method: 'completion/complete', + params: { + ref: { type: 'ref/resource', uri: 'api://v1/{tenant}/{resource}/{id}' }, + argument: { name: 'id', value: '123' }, + context: { + arguments: { + '{tenant}': 'acme-corp', + '{resource}': 'users' + } + } + } + }; + + const result = CompleteRequestSchema.safeParse(request); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.params.context?.arguments).toEqual({ + '{tenant}': 'acme-corp', + '{resource}': 'users' + }); + } + }); + }); + + describe('ToolSchema - JSON Schema 2020-12 support', () => { + test('should accept inputSchema with $schema field', () => { + const tool = { + name: 'test', + inputSchema: { + $schema: 'https://json-schema.org/draft/2020-12/schema', + type: 'object', + properties: { name: { type: 'string' } } + } + }; + const result = ToolSchema.safeParse(tool); + expect(result.success).toBe(true); + }); + + test('should accept inputSchema with additionalProperties', () => { + const tool = { + name: 'test', + inputSchema: { + type: 'object', + properties: { name: { type: 'string' } }, + additionalProperties: false + } + }; + const result = ToolSchema.safeParse(tool); + expect(result.success).toBe(true); + }); + + test('should accept inputSchema with composition keywords', () => { + const tool = { + name: 'test', + inputSchema: { + type: 'object', + allOf: [{ properties: { a: { type: 'string' } } }, { properties: { b: { type: 'number' } } }] + } + }; + const result = ToolSchema.safeParse(tool); + expect(result.success).toBe(true); + }); + + test('should accept inputSchema with $ref and $defs', () => { + const tool = { + name: 'test', + inputSchema: { + type: 'object', + properties: { user: { $ref: '#/$defs/User' } }, + $defs: { + User: { type: 'object', properties: { name: { type: 'string' } } } + } + } + }; + const result = ToolSchema.safeParse(tool); + expect(result.success).toBe(true); + }); + + test('should accept inputSchema with metadata keywords', () => { + const tool = { + name: 'test', + inputSchema: { + type: 'object', + title: 'User Input', + description: 'Input parameters for user creation', + deprecated: false, + examples: [{ name: 'John' }], + properties: { name: { type: 'string' } } + } + }; + const result = ToolSchema.safeParse(tool); + expect(result.success).toBe(true); + }); + + test('should accept outputSchema with full JSON Schema features', () => { + const tool = { + name: 'test', + inputSchema: { type: 'object' }, + outputSchema: { + type: 'object', + properties: { + id: { type: 'string' }, + tags: { type: 'array' } + }, + required: ['id'], + additionalProperties: false, + minProperties: 1 + } + }; + const result = ToolSchema.safeParse(tool); + expect(result.success).toBe(true); + }); + + test('should still require type: object at root for inputSchema', () => { + const tool = { + name: 'test', + inputSchema: { + type: 'string' + } + }; + const result = ToolSchema.safeParse(tool); + expect(result.success).toBe(false); + }); + + test('should still require type: object at root for outputSchema', () => { + const tool = { + name: 'test', + inputSchema: { type: 'object' }, + outputSchema: { + type: 'array' + } + }; + const result = ToolSchema.safeParse(tool); + expect(result.success).toBe(false); + }); + + test('should accept simple minimal schema (backward compatibility)', () => { + const tool = { + name: 'test', + inputSchema: { + type: 'object', + properties: { name: { type: 'string' } }, + required: ['name'] + } + }; + const result = ToolSchema.safeParse(tool); + expect(result.success).toBe(true); + }); + }); + + describe('ToolUseContent', () => { + test('should validate a tool call content', () => { + const toolCall = { + type: 'tool_use', + id: 'call_123', + name: 'get_weather', + input: { city: 'San Francisco', units: 'celsius' } + }; + + const result = ToolUseContentSchema.safeParse(toolCall); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.type).toBe('tool_use'); + expect(result.data.id).toBe('call_123'); + expect(result.data.name).toBe('get_weather'); + expect(result.data.input).toEqual({ city: 'San Francisco', units: 'celsius' }); + } + }); + + test('should validate tool call with _meta', () => { + const toolCall = { + type: 'tool_use', + id: 'call_456', + name: 'search', + input: { query: 'test' }, + _meta: { custom: 'data' } + }; + + const result = ToolUseContentSchema.safeParse(toolCall); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data._meta).toEqual({ custom: 'data' }); + } + }); + + test('should fail validation for missing required fields', () => { + const invalidToolCall = { + type: 'tool_use', + name: 'test' + // missing id and input + }; + + const result = ToolUseContentSchema.safeParse(invalidToolCall); + expect(result.success).toBe(false); + }); + }); + + describe('ToolResultContent', () => { + test('should validate a tool result content', () => { + const toolResult = { + type: 'tool_result', + toolUseId: 'call_123', + structuredContent: { temperature: 72, condition: 'sunny' } + }; + + const result = ToolResultContentSchema.safeParse(toolResult); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.type).toBe('tool_result'); + expect(result.data.toolUseId).toBe('call_123'); + expect(result.data.structuredContent).toEqual({ temperature: 72, condition: 'sunny' }); + } + }); + + test('should validate tool result with error in content', () => { + const toolResult = { + type: 'tool_result', + toolUseId: 'call_456', + structuredContent: { error: 'API_ERROR', message: 'Service unavailable' }, + isError: true + }; + + const result = ToolResultContentSchema.safeParse(toolResult); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.structuredContent).toEqual({ error: 'API_ERROR', message: 'Service unavailable' }); + expect(result.data.isError).toBe(true); + } + }); + + test('should fail validation for missing required fields', () => { + const invalidToolResult = { + type: 'tool_result', + content: { data: 'test' } + // missing toolUseId + }; + + const result = ToolResultContentSchema.safeParse(invalidToolResult); + expect(result.success).toBe(false); + }); + }); + + describe('ToolChoice', () => { + test('should validate tool choice with mode auto', () => { + const toolChoice = { + mode: 'auto' + }; + + const result = ToolChoiceSchema.safeParse(toolChoice); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.mode).toBe('auto'); + } + }); + + test('should validate tool choice with mode required', () => { + const toolChoice = { + mode: 'required' + }; + + const result = ToolChoiceSchema.safeParse(toolChoice); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.mode).toBe('required'); + } + }); + + test('should validate empty tool choice', () => { + const toolChoice = {}; + + const result = ToolChoiceSchema.safeParse(toolChoice); + expect(result.success).toBe(true); + }); + + test('should fail validation for invalid mode', () => { + const invalidToolChoice = { + mode: 'invalid' + }; + + const result = ToolChoiceSchema.safeParse(invalidToolChoice); + expect(result.success).toBe(false); + }); + }); + + describe('SamplingMessage content types', () => { + test('should validate user message with text', () => { + const userMessage = { + role: 'user', + content: { type: 'text', text: "What's the weather?" } + }; + + const result = SamplingMessageSchema.safeParse(userMessage); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.role).toBe('user'); + if (!Array.isArray(result.data.content)) { + expect(result.data.content.type).toBe('text'); + } + } + }); + + test('should validate user message with tool result', () => { + const userMessage = { + role: 'user', + content: { + type: 'tool_result', + toolUseId: 'call_123', + content: [] + } + }; + + const result = SamplingMessageSchema.safeParse(userMessage); + expect(result.success).toBe(true); + if (result.success && !Array.isArray(result.data.content)) { + expect(result.data.content.type).toBe('tool_result'); + } + }); + + test('should validate assistant message with text', () => { + const assistantMessage = { + role: 'assistant', + content: { type: 'text', text: "I'll check the weather for you." } + }; + + const result = SamplingMessageSchema.safeParse(assistantMessage); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.role).toBe('assistant'); + } + }); + + test('should validate assistant message with tool call', () => { + const assistantMessage = { + role: 'assistant', + content: { + type: 'tool_use', + id: 'call_123', + name: 'get_weather', + input: { city: 'SF' } + } + }; + + const result = SamplingMessageSchema.safeParse(assistantMessage); + expect(result.success).toBe(true); + if (result.success && !Array.isArray(result.data.content)) { + expect(result.data.content.type).toBe('tool_use'); + } + }); + + test('should validate any content type for any role', () => { + // The simplified schema allows any content type for any role + const assistantWithToolResult = { + role: 'assistant', + content: { + type: 'tool_result', + toolUseId: 'call_123', + content: [] + } + }; + + const result1 = SamplingMessageSchema.safeParse(assistantWithToolResult); + expect(result1.success).toBe(true); + + const userWithToolUse = { + role: 'user', + content: { + type: 'tool_use', + id: 'call_123', + name: 'test', + input: {} + } + }; + + const result2 = SamplingMessageSchema.safeParse(userWithToolUse); + expect(result2.success).toBe(true); + }); + }); + + describe('SamplingMessage', () => { + test('should validate user message via discriminated union', () => { + const message = { + role: 'user', + content: { type: 'text', text: 'Hello' } + }; + + const result = SamplingMessageSchema.safeParse(message); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.role).toBe('user'); + } + }); + + test('should validate assistant message via discriminated union', () => { + const message = { + role: 'assistant', + content: { type: 'text', text: 'Hi there!' } + }; + + const result = SamplingMessageSchema.safeParse(message); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.role).toBe('assistant'); + } + }); + }); + + describe('CreateMessageRequest', () => { + test('should validate request without tools', () => { + const request = { + method: 'sampling/createMessage', + params: { + messages: [{ role: 'user', content: { type: 'text', text: 'Hello' } }], + maxTokens: 1000 + } + }; + + const result = CreateMessageRequestSchema.safeParse(request); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.params.tools).toBeUndefined(); + } + }); + + test('should validate request with tools', () => { + const request = { + method: 'sampling/createMessage', + params: { + messages: [{ role: 'user', content: { type: 'text', text: "What's the weather?" } }], + maxTokens: 1000, + tools: [ + { + name: 'get_weather', + description: 'Get weather for a location', + inputSchema: { + type: 'object', + properties: { + location: { type: 'string' } + }, + required: ['location'] + } + } + ], + toolChoice: { + mode: 'auto' + } + } + }; + + const result = CreateMessageRequestSchema.safeParse(request); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.params.tools).toHaveLength(1); + expect(result.data.params.toolChoice?.mode).toBe('auto'); + } + }); + + test('should validate request with includeContext (soft-deprecated)', () => { + const request = { + method: 'sampling/createMessage', + params: { + messages: [{ role: 'user', content: { type: 'text', text: 'Help' } }], + maxTokens: 1000, + includeContext: 'thisServer' + } + }; + + const result = CreateMessageRequestSchema.safeParse(request); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.params.includeContext).toBe('thisServer'); + } + }); + }); + + describe('CreateMessageResult', () => { + test('should validate result with text content', () => { + const result = { + model: 'claude-3-5-sonnet-20241022', + role: 'assistant', + content: { type: 'text', text: "Here's the answer." }, + stopReason: 'endTurn' + }; + + const parseResult = CreateMessageResultSchema.safeParse(result); + expect(parseResult.success).toBe(true); + if (parseResult.success) { + expect(parseResult.data.role).toBe('assistant'); + expect(parseResult.data.stopReason).toBe('endTurn'); + } + }); + + test('should validate result with tool call (using WithTools schema)', () => { + const result = { + model: 'claude-3-5-sonnet-20241022', + role: 'assistant', + content: { + type: 'tool_use', + id: 'call_123', + name: 'get_weather', + input: { city: 'SF' } + }, + stopReason: 'toolUse' + }; + + // Tool call results use CreateMessageResultWithToolsSchema + const parseResult = CreateMessageResultWithToolsSchema.safeParse(result); + expect(parseResult.success).toBe(true); + if (parseResult.success) { + expect(parseResult.data.stopReason).toBe('toolUse'); + const content = parseResult.data.content; + expect(Array.isArray(content)).toBe(false); + if (!Array.isArray(content)) { + expect(content.type).toBe('tool_use'); + } + } + + // Basic CreateMessageResultSchema should NOT accept tool_use content + const basicResult = CreateMessageResultSchema.safeParse(result); + expect(basicResult.success).toBe(false); + }); + + test('should validate result with array content (using WithTools schema)', () => { + const result = { + model: 'claude-3-5-sonnet-20241022', + role: 'assistant', + content: [ + { type: 'text', text: 'Let me check the weather.' }, + { + type: 'tool_use', + id: 'call_123', + name: 'get_weather', + input: { city: 'SF' } + } + ], + stopReason: 'toolUse' + }; + + // Array content uses CreateMessageResultWithToolsSchema + const parseResult = CreateMessageResultWithToolsSchema.safeParse(result); + expect(parseResult.success).toBe(true); + if (parseResult.success) { + expect(parseResult.data.stopReason).toBe('toolUse'); + const content = parseResult.data.content; + expect(Array.isArray(content)).toBe(true); + if (Array.isArray(content)) { + expect(content).toHaveLength(2); + expect(content[0]?.type).toBe('text'); + expect(content[1]?.type).toBe('tool_use'); + } + } + + // Basic CreateMessageResultSchema should NOT accept array content + const basicResult = CreateMessageResultSchema.safeParse(result); + expect(basicResult.success).toBe(false); + }); + + test('should validate all new stop reasons', () => { + const stopReasons = ['endTurn', 'stopSequence', 'maxTokens', 'toolUse', 'refusal', 'other']; + + for (const stopReason of stopReasons) { + const result = { + model: 'test', + role: 'assistant', + content: { type: 'text', text: 'test' }, + stopReason + }; + + const parseResult = CreateMessageResultSchema.safeParse(result); + expect(parseResult.success).toBe(true); + } + }); + + test('should allow custom stop reason string', () => { + const result = { + model: 'test', + role: 'assistant', + content: { type: 'text', text: 'test' }, + stopReason: 'custom_provider_reason' + }; + + const parseResult = CreateMessageResultSchema.safeParse(result); + expect(parseResult.success).toBe(true); + }); + }); + + describe('ClientCapabilities with sampling', () => { + test('should validate capabilities with sampling.tools', () => { + const capabilities = { + sampling: { + tools: {} + } + }; + + const result = ClientCapabilitiesSchema.safeParse(capabilities); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.sampling?.tools).toBeDefined(); + } + }); + + test('should validate capabilities with sampling.context', () => { + const capabilities = { + sampling: { + context: {} + } + }; + + const result = ClientCapabilitiesSchema.safeParse(capabilities); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.sampling?.context).toBeDefined(); + } + }); + + test('should validate capabilities with both', () => { + const capabilities = { + sampling: { + context: {}, + tools: {} + } + }; + + const result = ClientCapabilitiesSchema.safeParse(capabilities); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.sampling?.context).toBeDefined(); + expect(result.data.sampling?.tools).toBeDefined(); + } + }); + }); + + describe('ElicitRequestFormParamsSchema', () => { + test('accepts requestedSchema with extra JSON Schema metadata keys', () => { + // Mirrors what z.toJSONSchema() emits — includes $schema, additionalProperties, etc. + // See https://github.com/modelcontextprotocol/typescript-sdk/issues/1362 + const params = { + message: 'Please provide your name', + requestedSchema: { + $schema: 'https://json-schema.org/draft/2020-12/schema', + type: 'object', + properties: { + name: { type: 'string' } + }, + required: ['name'], + additionalProperties: false + } + }; + + const result = ElicitRequestFormParamsSchema.safeParse(params); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.requestedSchema.type).toBe('object'); + expect(result.data.requestedSchema.$schema).toBe('https://json-schema.org/draft/2020-12/schema'); + expect(result.data.requestedSchema.additionalProperties).toBe(false); + } + }); + }); +}); + +describe('2025-11-25 task wire interop (task feature removed; wire types remain)', () => { + test('tasks/get parses through the client request union', () => { + const result = ClientRequestSchema.safeParse({ method: 'tasks/get', params: { taskId: 'task-123' } }); + expect(result.success).toBe(true); + }); + + test('task-augmented tools/call params parse and retain the task field', () => { + const result = CallToolRequestSchema.safeParse({ + method: 'tools/call', + params: { name: 'echo', arguments: {}, task: { ttl: 60000 } } + }); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.params.task).toEqual({ ttl: 60000 }); + } + }); + + test('tool execution.taskSupport is accepted', () => { + const result = ToolSchema.safeParse({ + name: 'echo', + inputSchema: { type: 'object' }, + execution: { taskSupport: 'optional' } + }); + expect(result.success).toBe(true); + }); + + test('capabilities.tasks is retained, not stripped', () => { + const result = ClientCapabilitiesSchema.safeParse({ tasks: { list: {}, cancel: {} } }); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.tasks).toEqual({ list: {}, cancel: {} }); + } + }); +}); + +describe('2026-07-28 wire shapes', () => { + describe('RequestMetaEnvelope', () => { + const envelope = { + [PROTOCOL_VERSION_META_KEY]: '2026-07-28', + [CLIENT_INFO_META_KEY]: { name: 'test-client', version: '1.0.0' }, + [CLIENT_CAPABILITIES_META_KEY]: {} + }; + + test('accepts a complete envelope', () => { + const result = RequestMetaEnvelopeSchema.safeParse(envelope); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data[PROTOCOL_VERSION_META_KEY]).toBe('2026-07-28'); + expect(result.data[CLIENT_INFO_META_KEY]).toEqual({ name: 'test-client', version: '1.0.0' }); + expect(result.data[CLIENT_CAPABILITIES_META_KEY]).toEqual({}); + } + }); + + test('accepts the optional log level, progress token, and unknown keys', () => { + const result = RequestMetaEnvelopeSchema.safeParse({ + ...envelope, + [LOG_LEVEL_META_KEY]: 'warning', + progressToken: 'token-1', + 'com.example/custom': { anything: true } + }); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data[LOG_LEVEL_META_KEY]).toBe('warning'); + expect(result.data.progressToken).toBe('token-1'); + expect(result.data['com.example/custom']).toEqual({ anything: true }); + } + }); + + test.each([PROTOCOL_VERSION_META_KEY, CLIENT_INFO_META_KEY, CLIENT_CAPABILITIES_META_KEY])( + 'rejects an envelope missing %s', + key => { + const incomplete: Record = { ...envelope }; + delete incomplete[key]; + expect(RequestMetaEnvelopeSchema.safeParse(incomplete).success).toBe(false); + } + ); + + test('rejects an invalid log level', () => { + const result = RequestMetaEnvelopeSchema.safeParse({ ...envelope, [LOG_LEVEL_META_KEY]: 'loud' }); + expect(result.success).toBe(false); + }); + }); + + describe('DiscoverRequest', () => { + test('parses a discover request with and without params', () => { + expect(DiscoverRequestSchema.safeParse({ method: 'server/discover' }).success).toBe(true); + expect( + DiscoverRequestSchema.safeParse({ + method: 'server/discover', + params: { _meta: { [PROTOCOL_VERSION_META_KEY]: '2026-07-28' } } + }).success + ).toBe(true); + }); + + test('rejects other methods', () => { + expect(DiscoverRequestSchema.safeParse({ method: 'initialize' }).success).toBe(false); + }); + }); + + describe('DiscoverResult', () => { + const result = { + supportedVersions: ['2026-07-28'], + capabilities: { tools: { listChanged: true } }, + serverInfo: { name: 'test-server', version: '1.0.0' } + }; + + test('parses a discover result', () => { + const parsed = DiscoverResultSchema.safeParse({ ...result, resultType: 'complete', instructions: 'Use the echo tool.' }); + expect(parsed.success).toBe(true); + if (parsed.success) { + expect(parsed.data.supportedVersions).toEqual(['2026-07-28']); + expect(parsed.data.capabilities).toEqual({ tools: { listChanged: true } }); + expect(parsed.data.serverInfo).toEqual({ name: 'test-server', version: '1.0.0' }); + expect(parsed.data.instructions).toBe('Use the echo tool.'); + } + }); + + test.each(['supportedVersions', 'capabilities', 'serverInfo'])('rejects a discover result missing %s', key => { + const incomplete: Record = { ...result }; + delete incomplete[key]; + expect(DiscoverResultSchema.safeParse(incomplete).success).toBe(false); + }); + }); + + describe('Result resultType passthrough', () => { + test('accepts results with and without resultType (absent means "complete")', () => { + const withIt = ResultSchema.safeParse({ resultType: 'complete' }); + expect(withIt.success).toBe(true); + if (withIt.success) { + expect(withIt.data.resultType).toBe('complete'); + } + const withoutIt = ResultSchema.safeParse({}); + expect(withoutIt.success).toBe(true); + if (withoutIt.success) { + expect(withoutIt.data.resultType).toBeUndefined(); + } + }); + + test('rejects a non-string resultType', () => { + expect(ResultSchema.safeParse({ resultType: 42 }).success).toBe(false); + }); + + test('EmptyResult accepts resultType but still rejects unknown keys', () => { + expect(EmptyResultSchema.safeParse({ resultType: 'complete' }).success).toBe(true); + expect(EmptyResultSchema.safeParse({ unexpected: true }).success).toBe(false); + }); + }); +}); diff --git a/packages/core-internal/test/types/errors.test.ts b/packages/core-internal/test/types/errors.test.ts new file mode 100644 index 0000000000..1072537d97 --- /dev/null +++ b/packages/core-internal/test/types/errors.test.ts @@ -0,0 +1,44 @@ +import { describe, expect, it } from 'vitest'; + +import { ProtocolErrorCode } from '../../src/types/enums'; +import { ProtocolError, UnsupportedProtocolVersionError } from '../../src/types/errors'; + +describe('UnsupportedProtocolVersionError', () => { + const data = { supported: ['2025-11-25', '2025-06-18'], requested: '2026-07-28' }; + + it('carries code -32004 and the supported/requested data', () => { + const error = new UnsupportedProtocolVersionError(data); + expect(error.code).toBe(ProtocolErrorCode.UnsupportedProtocolVersion); + expect(error.code).toBe(-32004); + expect(error.supported).toEqual(['2025-11-25', '2025-06-18']); + expect(error.requested).toBe('2026-07-28'); + expect(error.data).toEqual(data); + }); + + it('defaults the message from the requested version', () => { + const error = new UnsupportedProtocolVersionError(data); + expect(error.message).toBe('Unsupported protocol version: 2026-07-28'); + const custom = new UnsupportedProtocolVersionError(data, 'try another version'); + expect(custom.message).toBe('try another version'); + }); + + it('is materialized by ProtocolError.fromError', () => { + const error = ProtocolError.fromError(-32004, 'Unsupported protocol version: 2026-07-28', data); + expect(error).toBeInstanceOf(UnsupportedProtocolVersionError); + if (error instanceof UnsupportedProtocolVersionError) { + expect(error.supported).toEqual(['2025-11-25', '2025-06-18']); + expect(error.requested).toBe('2026-07-28'); + } + expect(error.message).toBe('Unsupported protocol version: 2026-07-28'); + }); + + it('falls back to a generic ProtocolError when the data is missing or malformed', () => { + for (const malformed of [undefined, {}, { supported: 'not-an-array', requested: '2026-07-28' }, { supported: ['2025-11-25'] }]) { + const error = ProtocolError.fromError(-32004, 'unsupported', malformed); + expect(error).toBeInstanceOf(ProtocolError); + expect(error).not.toBeInstanceOf(UnsupportedProtocolVersionError); + expect(error.code).toBe(-32004); + expect(error.data).toEqual(malformed); + } + }); +}); diff --git a/packages/core-internal/test/types/guards.test.ts b/packages/core-internal/test/types/guards.test.ts new file mode 100644 index 0000000000..fe96b64dd3 --- /dev/null +++ b/packages/core-internal/test/types/guards.test.ts @@ -0,0 +1,123 @@ +import { describe, expect, it } from 'vitest'; + +import { JSONRPC_VERSION } from '../../src/types/constants'; +import { isCallToolResult, isJSONRPCErrorResponse, isJSONRPCResponse, isJSONRPCResultResponse } from '../../src/types/guards'; + +describe('isJSONRPCResponse', () => { + it('returns true for a valid result response', () => { + expect( + isJSONRPCResponse({ + jsonrpc: JSONRPC_VERSION, + id: 1, + result: {} + }) + ).toBe(true); + }); + + it('returns true for a valid error response', () => { + expect( + isJSONRPCResponse({ + jsonrpc: JSONRPC_VERSION, + id: 1, + error: { code: -32_600, message: 'Invalid Request' } + }) + ).toBe(true); + }); + + it('returns false for a request', () => { + expect( + isJSONRPCResponse({ + jsonrpc: JSONRPC_VERSION, + id: 1, + method: 'test' + }) + ).toBe(false); + }); + + it('returns false for a notification', () => { + expect( + isJSONRPCResponse({ + jsonrpc: JSONRPC_VERSION, + method: 'test' + }) + ).toBe(false); + }); + + it('returns false for arbitrary objects', () => { + expect(isJSONRPCResponse({ foo: 'bar' })).toBe(false); + }); + + it('narrows the type correctly', () => { + const value: unknown = { + jsonrpc: JSONRPC_VERSION, + id: 1, + result: { content: [] } + }; + if (isJSONRPCResponse(value)) { + // Type should be narrowed to JSONRPCResponse + expect(value.jsonrpc).toBe(JSONRPC_VERSION); + expect(value.id).toBe(1); + } + }); + + it('agrees with isJSONRPCResultResponse || isJSONRPCErrorResponse', () => { + const values = [ + { jsonrpc: JSONRPC_VERSION, id: 1, result: {} }, + { jsonrpc: JSONRPC_VERSION, id: 2, error: { code: -1, message: 'err' } }, + { jsonrpc: JSONRPC_VERSION, id: 3, method: 'test' }, + { jsonrpc: JSONRPC_VERSION, method: 'notify' }, + { foo: 'bar' }, + null, + 42 + ]; + for (const v of values) { + expect(isJSONRPCResponse(v)).toBe(isJSONRPCResultResponse(v) || isJSONRPCErrorResponse(v)); + } + }); +}); + +describe('isCallToolResult', () => { + it('returns false for an empty object (content is required)', () => { + expect(isCallToolResult({})).toBe(false); + }); + + it('returns true for a result with content', () => { + expect( + isCallToolResult({ + content: [{ type: 'text', text: 'hello' }] + }) + ).toBe(true); + }); + + it('returns true for a result with isError', () => { + expect( + isCallToolResult({ + content: [{ type: 'text', text: 'fail' }], + isError: true + }) + ).toBe(true); + }); + + it('returns true for a result with structuredContent', () => { + expect( + isCallToolResult({ + content: [], + structuredContent: { key: 'value' } + }) + ).toBe(true); + }); + + it('returns false for non-objects', () => { + expect(isCallToolResult(null)).toBe(false); + expect(isCallToolResult(42)).toBe(false); + expect(isCallToolResult('string')).toBe(false); + }); + + it('returns false for invalid content items', () => { + expect( + isCallToolResult({ + content: [{ type: 'invalid' }] + }) + ).toBe(false); + }); +}); diff --git a/packages/core-internal/test/types/specTypeSchema.test.ts b/packages/core-internal/test/types/specTypeSchema.test.ts new file mode 100644 index 0000000000..e04e9f1c45 --- /dev/null +++ b/packages/core-internal/test/types/specTypeSchema.test.ts @@ -0,0 +1,177 @@ +import { describe, expect, expectTypeOf, it } from 'vitest'; + +import type { OAuthMetadata, OAuthTokens } from '../../src/shared/auth'; +import * as schemas from '../../src/types/schemas'; +import type { SpecTypeName, SpecTypes } from '../../src/types/specTypeSchema'; +import { isSpecType, specTypeSchemas } from '../../src/types/specTypeSchema'; +import type { + CallToolResult, + ContentBlock, + Implementation, + JSONObject, + JSONRPCRequest, + JSONValue, + ResourceTemplateType, + Tool +} from '../../src/types/types'; + +describe('specTypeSchemas', () => { + it('returns a StandardSchemaV1Sync validator that accepts valid values', () => { + const result = specTypeSchemas.Implementation['~standard'].validate({ name: 'x', version: '1.0.0' }); + expect(result.issues).toBeUndefined(); + }); + + it('returns a validator that rejects invalid values with issues', () => { + const result = specTypeSchemas.Implementation['~standard'].validate({ name: 'x' }); + expect(result.issues?.length).toBeGreaterThan(0); + }); + + it('rejects unknown names at compile time and is undefined at runtime', () => { + // @ts-expect-error - 'NotASpecType' is not a SpecTypeName + expect(specTypeSchemas['NotASpecType']).toBeUndefined(); + }); + + it('covers JSON-RPC envelope types', () => { + const ok = specTypeSchemas.JSONRPCRequest['~standard'].validate({ jsonrpc: '2.0', id: 1, method: 'ping' }); + expect(ok.issues).toBeUndefined(); + }); + + it('covers OAuth types from shared/auth.ts', () => { + const ok = specTypeSchemas.OAuthTokens['~standard'].validate({ access_token: 'x', token_type: 'Bearer' }); + expect(ok.issues).toBeUndefined(); + const bad = specTypeSchemas.OAuthTokens['~standard'].validate({ token_type: 'Bearer' }); + expect(bad.issues?.length).toBeGreaterThan(0); + }); +}); + +describe('isSpecType', () => { + it('CallToolResult — accepts valid, rejects invalid/null/primitive', () => { + expect(isSpecType.CallToolResult({ content: [{ type: 'text', text: 'hi' }] })).toBe(true); + expect(isSpecType.CallToolResult({ content: 'not-an-array' })).toBe(false); + expect(isSpecType.CallToolResult(null)).toBe(false); + expect(isSpecType.CallToolResult('string')).toBe(false); + }); + + it('ContentBlock — accepts text block, rejects wrong shape', () => { + expect(isSpecType.ContentBlock({ type: 'text', text: 'hi' })).toBe(true); + expect(isSpecType.ContentBlock({ type: 'text' })).toBe(false); + expect(isSpecType.ContentBlock({})).toBe(false); + }); + + it('Tool — accepts valid, rejects missing inputSchema', () => { + expect(isSpecType.Tool({ name: 'echo', inputSchema: { type: 'object' } })).toBe(true); + expect(isSpecType.Tool({ name: 'echo' })).toBe(false); + }); + + it('ResourceTemplate — accepts valid, rejects missing uriTemplate', () => { + expect(isSpecType.ResourceTemplate({ name: 'r', uriTemplate: 'file:///{path}' })).toBe(true); + expect(isSpecType.ResourceTemplate({ name: 'r' })).toBe(false); + }); + + it('rejects unknown names at compile time and is undefined at runtime', () => { + // @ts-expect-error - 'NotASpecType' is not a SpecTypeName + expect(isSpecType['NotASpecType']).toBeUndefined(); + }); + + it('excludes internal helper schemas (no matching public type)', () => { + // @ts-expect-error - ListChangedOptionsBase is internal-only + expect(isSpecType['ListChangedOptionsBase']).toBeUndefined(); + // @ts-expect-error - BaseRequestParams is internal-only + expect(specTypeSchemas['BaseRequestParams']).toBeUndefined(); + // @ts-expect-error - NotificationsParams is internal-only + expect(isSpecType['NotificationsParams']).toBeUndefined(); + }); + + it('narrows the value type to the schema input type', () => { + const v: unknown = { name: 'x', version: '1.0.0' }; + if (isSpecType.Implementation(v)) { + // ImplementationSchema has no defaults/transforms, so its input type equals Implementation. + expectTypeOf(v).toEqualTypeOf(); + } + }); + + it('narrows to the input type, not the output type, for schemas with defaults', () => { + const v: unknown = {}; + expect(isSpecType.CallToolResult(v)).toBe(true); + if (isSpecType.CallToolResult(v)) { + // CallToolResultSchema has `content: z.array(...).default([])`, so the input type + // permits `content` to be absent. The guard narrows to that input shape. + expectTypeOf(v.content).toEqualTypeOf(); + expectTypeOf(v).not.toEqualTypeOf(); + } + }); + + it('JSONValue / JSONObject — narrows to the JSON type, not unknown', () => { + // These schemas use an explicit z.ZodType annotation for recursion; without the + // second param Zod's Input defaults to `unknown` and the predicate would not narrow. + const v: unknown = { a: 1 }; + if (isSpecType.JSONValue(v)) { + expectTypeOf(v).toEqualTypeOf(); + } + if (isSpecType.JSONObject(v)) { + expectTypeOf(v).toEqualTypeOf(); + } + }); + + it('guards work as filter callbacks and narrow the element type', () => { + const mixed: unknown[] = [{ type: 'text', text: 'hi' }, 42, { type: 'text' }]; + const blocks = mixed.filter(isSpecType.ContentBlock); + expect(blocks).toHaveLength(1); + expectTypeOf(blocks).toEqualTypeOf(); + }); +}); + +describe('SpecTypeName / SpecTypes (type-level)', () => { + it('SpecTypeName includes representative names', () => { + expectTypeOf<'CallToolResult'>().toMatchTypeOf(); + expectTypeOf<'ContentBlock'>().toMatchTypeOf(); + expectTypeOf<'Tool'>().toMatchTypeOf(); + expectTypeOf<'Implementation'>().toMatchTypeOf(); + expectTypeOf<'JSONRPCRequest'>().toMatchTypeOf(); + expectTypeOf<'OAuthTokens'>().toMatchTypeOf(); + expectTypeOf<'OAuthMetadata'>().toMatchTypeOf(); + expectTypeOf<'ResourceTemplate'>().toMatchTypeOf(); + }); + + it('SpecTypes[K] matches the named export type', () => { + expectTypeOf().toEqualTypeOf(); + expectTypeOf().toEqualTypeOf(); + expectTypeOf().toEqualTypeOf(); + expectTypeOf().toEqualTypeOf(); + expectTypeOf().toEqualTypeOf(); + expectTypeOf().toEqualTypeOf(); + expectTypeOf().toEqualTypeOf(); + // The public type is exported as ResourceTemplateType (the bare name collides with the + // server package's ResourceTemplate class), so this is the one entry where the key and + // the public type name differ. + expectTypeOf().toEqualTypeOf(); + }); +}); + +describe('SPEC_SCHEMA_KEYS allowlist', () => { + // Mirrors the exclusion comment in specTypeSchema.ts. If this list grows, confirm the new + // entry has no public type in types.ts before adding it here; otherwise add it to the allowlist. + const INTERNAL_HELPER_SCHEMAS: readonly string[] = [ + 'ListChangedOptionsBaseSchema', + 'BaseRequestParamsSchema', + 'NotificationsParamsSchema', + 'ClientTasksCapabilitySchema', + 'ServerTasksCapabilitySchema' + ]; + + it('covers every public protocol schema in schemas.ts (drift guard)', () => { + // PascalCase filters out helper functions like getRequestSchema/getResultSchema. + const allProtocolSchemas = Object.keys(schemas).filter(k => k.endsWith('Schema') && /^[A-Z]/.test(k)); + const expected = allProtocolSchemas + .filter(k => !INTERNAL_HELPER_SCHEMAS.includes(k)) + .map(k => k.slice(0, -'Schema'.length)) + .sort(); + // Auth schemas are sourced from shared/auth.ts, not schemas.ts. Keep only the protocol entries + // (whose `*Schema` const lives in schemas.ts) so the comparison stays against schemas.ts — + // robust to new auth schemas (e.g. IdJagTokenExchangeResponse) without a name-prefix heuristic. + const actual = Object.keys(isSpecType) + .filter(k => `${k}Schema` in schemas) + .sort(); + expect(actual).toEqual(expected); + }); +}); diff --git a/packages/core-internal/test/util/standardSchema.test.ts b/packages/core-internal/test/util/standardSchema.test.ts new file mode 100644 index 0000000000..8856592ff0 --- /dev/null +++ b/packages/core-internal/test/util/standardSchema.test.ts @@ -0,0 +1,42 @@ +import * as z from 'zod/v4'; + +import { standardSchemaToJsonSchema } from '../../src/util/standardSchema'; + +describe('standardSchemaToJsonSchema', () => { + test('emits type:object for plain z.object schemas', () => { + const schema = z.object({ name: z.string(), age: z.number() }); + const result = standardSchemaToJsonSchema(schema, 'input'); + + expect(result.type).toBe('object'); + expect(result.properties).toBeDefined(); + }); + + test('emits type:object for discriminated unions', () => { + const schema = z.discriminatedUnion('action', [ + z.object({ action: z.literal('create'), name: z.string() }), + z.object({ action: z.literal('delete'), id: z.string() }) + ]); + const result = standardSchemaToJsonSchema(schema, 'input'); + + expect(result.type).toBe('object'); + // Zod emits oneOf for discriminated unions; the catchall on Tool.inputSchema + // accepts it, but the top-level type must be present per MCP spec. + expect(result.oneOf ?? result.anyOf).toBeDefined(); + }); + + test('throws for schemas with explicit non-object type', () => { + expect(() => standardSchemaToJsonSchema(z.string(), 'input')).toThrow(/must describe objects/); + expect(() => standardSchemaToJsonSchema(z.array(z.string()), 'input')).toThrow(/must describe objects/); + expect(() => standardSchemaToJsonSchema(z.number(), 'input')).toThrow(/must describe objects/); + }); + + test('preserves existing type:object without modification', () => { + const schema = z.object({ x: z.string() }); + const result = standardSchemaToJsonSchema(schema, 'input'); + + // Spread order means zod's own type:"object" wins; verify no double-wrap. + const keys = Object.keys(result); + expect(keys.filter(k => k === 'type')).toHaveLength(1); + expect(result.type).toBe('object'); + }); +}); diff --git a/packages/core-internal/test/util/standardSchema.zodFallback.test.ts b/packages/core-internal/test/util/standardSchema.zodFallback.test.ts new file mode 100644 index 0000000000..f8862b08a3 --- /dev/null +++ b/packages/core-internal/test/util/standardSchema.zodFallback.test.ts @@ -0,0 +1,37 @@ +import { describe, expect, it, vi } from 'vitest'; +import * as z from 'zod/v4'; +import { standardSchemaToJsonSchema } from '../../src/util/standardSchema'; + +type SchemaArg = Parameters[0]; + +describe('standardSchemaToJsonSchema — zod fallback paths', () => { + it('falls back to z.toJSONSchema for zod 4.0–4.1 (vendor=zod, no ~standard.jsonSchema, has _zod)', () => { + const warn = vi.spyOn(console, 'warn').mockImplementation(() => {}); + const real = z.object({ a: z.string() }); + // Simulate zod 4.0–4.1: shadow `~standard` on the real instance with `jsonSchema` removed. + // Keeps the rest of the zod 4 object (including `_zod`) intact so z.toJSONSchema can introspect it. + const { jsonSchema: _drop, ...stdNoJson } = real['~standard'] as unknown as Record; + void _drop; + Object.defineProperty(real, '~standard', { value: { ...stdNoJson, vendor: 'zod' }, configurable: true }); + + const result = standardSchemaToJsonSchema(real as unknown as SchemaArg); + expect(result.type).toBe('object'); + expect((result.properties as unknown as Record)?.a).toBeDefined(); + expect(warn).toHaveBeenCalledOnce(); + expect(warn.mock.calls[0]?.[0]).toContain('zod 4.2.0'); + warn.mockRestore(); + }); + + it('throws a clear error for zod 3 (vendor=zod, no ~standard.jsonSchema, no _zod)', () => { + // zod 3.24+ reports `~standard.vendor === 'zod'` but has no `_zod` internal marker. + const zod3ish = { _def: {}, '~standard': { version: 1, vendor: 'zod', validate: () => ({ value: {} }) } }; + expect(() => standardSchemaToJsonSchema(zod3ish as unknown as SchemaArg)).toThrow(/zod 3/); + expect(() => standardSchemaToJsonSchema(zod3ish as unknown as SchemaArg)).toThrow(/4\.2\.0/); + }); + + it('throws a clear error for non-zod libraries without ~standard.jsonSchema', () => { + const fake = { '~standard': { version: 1, vendor: 'mylib', validate: () => ({ value: {} }) } }; + expect(() => standardSchemaToJsonSchema(fake as unknown as SchemaArg)).toThrow(/mylib/); + expect(() => standardSchemaToJsonSchema(fake as unknown as SchemaArg)).toThrow(/fromJsonSchema/); + }); +}); diff --git a/packages/core-internal/test/util/zodCompat.test.ts b/packages/core-internal/test/util/zodCompat.test.ts new file mode 100644 index 0000000000..5bdc229298 --- /dev/null +++ b/packages/core-internal/test/util/zodCompat.test.ts @@ -0,0 +1,89 @@ +import { vi } from 'vitest'; +import * as z from 'zod/v4'; + +import { standardSchemaToJsonSchema } from '../../src/util/standardSchema'; +import { isZodRawShape, normalizeRawShapeSchema } from '../../src/util/zodCompat'; + +describe('isZodRawShape', () => { + test('treats empty object as a raw shape (matches v1)', () => { + expect(isZodRawShape({})).toBe(true); + }); + test('detects raw shape with zod fields', () => { + expect(isZodRawShape({ a: z.string() })).toBe(true); + }); + test('rejects a Standard Schema instance', () => { + expect(isZodRawShape(z.object({ a: z.string() }))).toBe(false); + }); + test('rejects a shape with non-Zod Standard Schema fields', () => { + const nonZod = { '~standard': { version: 1, vendor: 'arktype', validate: () => ({ value: 'x' }) } }; + expect(isZodRawShape({ a: nonZod })).toBe(false); + }); + test('rejects a shape with Zod v3 fields (only v4 is wrappable)', () => { + expect(isZodRawShape({ a: mockZodV3String() })).toBe(false); + }); + test('rejects non-plain objects with no own-enumerable properties', () => { + expect(isZodRawShape([])).toBe(false); + expect(isZodRawShape([z.string()])).toBe(false); + expect(isZodRawShape(new Date())).toBe(false); + expect(isZodRawShape(new Map())).toBe(false); + expect(isZodRawShape(/regex/)).toBe(false); + }); + test('accepts a null-prototype plain object', () => { + const o = Object.create(null); + o.a = z.string(); + expect(isZodRawShape(o)).toBe(true); + }); +}); + +// Minimal structural mock of a Zod v3 schema: has `_def.typeName` and +// `~standard.vendor === 'zod'` (zod >=3.24), but no `_zod`. +function mockZodV3String(): unknown { + return { + _def: { typeName: 'ZodString', checks: [], coerce: false }, + '~standard': { version: 1, vendor: 'zod', validate: (v: unknown) => ({ value: v }) }, + parse: (v: unknown) => v + }; +} + +describe('normalizeRawShapeSchema', () => { + test('wraps empty raw shape into z.object({})', () => { + const wrapped = normalizeRawShapeSchema({}); + expect(wrapped).toBeDefined(); + expect(standardSchemaToJsonSchema(wrapped!, 'input').type).toBe('object'); + }); + test('passes through an already-wrapped Standard Schema unchanged', () => { + const schema = z.object({ a: z.string() }); + expect(normalizeRawShapeSchema(schema)).toBe(schema); + }); + test('returns undefined for undefined input', () => { + expect(normalizeRawShapeSchema(undefined)).toBeUndefined(); + }); + test('throws TypeError for an invalid object that is neither raw shape nor Standard Schema', () => { + expect(() => normalizeRawShapeSchema({ a: 'not a zod schema' } as never)).toThrow(TypeError); + }); + test('passes through a Standard Schema without `~standard.jsonSchema` (per-vendor handling deferred to standardSchemaToJsonSchema)', () => { + const noJson = { '~standard': { version: 1, vendor: 'x', validate: () => ({ value: {} }) } }; + expect(normalizeRawShapeSchema(noJson as never)).toBe(noJson); + }); + test('passes through a zod 4.0-4.1 schema so standardSchemaToJsonSchema can apply its z.toJSONSchema fallback', () => { + const warn = vi.spyOn(console, 'warn').mockImplementation(() => {}); + const real = z.object({ a: z.string() }); + // Simulate zod 4.0-4.1: shadow `~standard` with `jsonSchema` removed, keep `_zod` intact. + const { jsonSchema: _drop, ...stdNoJson } = real['~standard'] as unknown as Record; + void _drop; + Object.defineProperty(real, '~standard', { value: { ...stdNoJson, vendor: 'zod' }, configurable: true }); + + const normalized = normalizeRawShapeSchema(real); + expect(normalized).toBe(real); + const json = standardSchemaToJsonSchema(normalized!, 'input'); + expect(json.type).toBe('object'); + expect((json.properties as Record)?.a).toBeDefined(); + warn.mockRestore(); + }); + test('throws actionable TypeError for a raw shape with Zod v3 fields', () => { + expect(() => normalizeRawShapeSchema({ a: mockZodV3String() } as never)).toThrow(/Zod v4 schemas.*Got a Zod v3 field schema/); + }); + test('throws the intended TypeError (not Object.values crash) for null input', () => { + expect(() => normalizeRawShapeSchema(null as never)).toThrow(/must be a Standard Schema/); + }); +}); diff --git a/packages/core-internal/test/validators/validators.test.ts b/packages/core-internal/test/validators/validators.test.ts new file mode 100644 index 0000000000..7ffb4d16dc --- /dev/null +++ b/packages/core-internal/test/validators/validators.test.ts @@ -0,0 +1,625 @@ +/** + * Tests all validator providers with various JSON Schema 2020-12 features + * Based on MCP specification for elicitation schemas: + * https://modelcontextprotocol.io/specification/draft/client/elicitation.md + */ + +import { readFileSync } from 'node:fs'; +import path from 'node:path'; + +import { vi } from 'vitest'; + +import { AjvJsonSchemaValidator } from '../../src/validators/ajvProvider'; +import { CfWorkerJsonSchemaValidator } from '../../src/validators/cfWorkerProvider'; +import type { JsonSchemaType } from '../../src/validators/types'; + +// Test with both AJV and CfWorker validators +// AJV validator will use default configuration with format validation enabled +const validators = [ + { name: 'AJV', provider: new AjvJsonSchemaValidator() }, + { name: 'CfWorker', provider: new CfWorkerJsonSchemaValidator() } +]; + +describe('JSON Schema Validators', () => { + describe.each(validators)('$name Validator', ({ provider }) => { + describe('String schemas', () => { + it('validates basic string', () => { + const schema: JsonSchemaType = { + type: 'string' + }; + const validator = provider.getValidator(schema); + + const validResult = validator('hello'); + expect(validResult.valid).toBe(true); + expect(validResult.data).toBe('hello'); + + const invalidResult = validator(123); + expect(invalidResult.valid).toBe(false); + expect(invalidResult.errorMessage).toBeDefined(); + }); + + it('validates string with title and description', () => { + const schema: JsonSchemaType = { + type: 'string', + title: 'Name', + description: "User's full name" + }; + const validator = provider.getValidator(schema); + + const result = validator('John Doe'); + expect(result.valid).toBe(true); + expect(result.data).toBe('John Doe'); + }); + + it('validates string with length constraints', () => { + const schema: JsonSchemaType = { + type: 'string', + minLength: 3, + maxLength: 10 + }; + const validator = provider.getValidator(schema); + + expect(validator('abc').valid).toBe(true); + expect(validator('abcdefghij').valid).toBe(true); + expect(validator('ab').valid).toBe(false); + expect(validator('abcdefghijk').valid).toBe(false); + }); + + it('validates email format', () => { + const schema: JsonSchemaType = { + type: 'string', + format: 'email' + }; + const validator = provider.getValidator(schema); + + expect(validator('user@example.com').valid).toBe(true); + expect(validator('invalid-email').valid).toBe(false); + }); + + it('validates URI format', () => { + const schema: JsonSchemaType = { + type: 'string', + format: 'uri' + }; + const validator = provider.getValidator(schema); + + expect(validator('https://example.com').valid).toBe(true); + expect(validator('not-a-uri').valid).toBe(false); + }); + + it('validates date-time format', () => { + const schema: JsonSchemaType = { + type: 'string', + format: 'date-time' + }; + const validator = provider.getValidator(schema); + + expect(validator('2025-10-17T12:00:00Z').valid).toBe(true); + expect(validator('not-a-date').valid).toBe(false); + }); + + it('validates string pattern', () => { + const schema: JsonSchemaType = { + type: 'string', + pattern: '^[A-Z]{3}$' + }; + const validator = provider.getValidator(schema); + + expect(validator('ABC').valid).toBe(true); + expect(validator('abc').valid).toBe(false); + expect(validator('ABCD').valid).toBe(false); + }); + }); + + describe('Number schemas', () => { + it('validates number type', () => { + const schema: JsonSchemaType = { + type: 'number' + }; + const validator = provider.getValidator(schema); + + expect(validator(42).valid).toBe(true); + expect(validator(3.14).valid).toBe(true); + expect(validator('42').valid).toBe(false); + }); + + it('validates integer type', () => { + const schema: JsonSchemaType = { + type: 'integer' + }; + const validator = provider.getValidator(schema); + + expect(validator(42).valid).toBe(true); + expect(validator(3.14).valid).toBe(false); + }); + + it('validates number range', () => { + const schema: JsonSchemaType = { + type: 'number', + minimum: 0, + maximum: 100 + }; + const validator = provider.getValidator(schema); + + expect(validator(0).valid).toBe(true); + expect(validator(50).valid).toBe(true); + expect(validator(100).valid).toBe(true); + expect(validator(-1).valid).toBe(false); + expect(validator(101).valid).toBe(false); + }); + }); + + describe('Boolean schemas', () => { + it('validates boolean type', () => { + const schema: JsonSchemaType = { + type: 'boolean' + }; + const validator = provider.getValidator(schema); + + expect(validator(true).valid).toBe(true); + expect(validator(false).valid).toBe(true); + expect(validator('true').valid).toBe(false); + expect(validator(1).valid).toBe(false); + }); + + it('validates boolean with default', () => { + const schema: JsonSchemaType = { + type: 'boolean', + default: false + }; + const validator = provider.getValidator(schema); + + expect(validator(true).valid).toBe(true); + expect(validator(false).valid).toBe(true); + }); + }); + + describe('Enum schemas', () => { + it('validates enum values', () => { + const schema: JsonSchemaType = { + enum: ['red', 'green', 'blue'] + }; + const validator = provider.getValidator(schema); + + expect(validator('red').valid).toBe(true); + expect(validator('green').valid).toBe(true); + expect(validator('blue').valid).toBe(true); + expect(validator('yellow').valid).toBe(false); + }); + + it('validates enum with mixed types', () => { + const schema: JsonSchemaType = { + enum: ['option1', 42, true, null] + }; + const validator = provider.getValidator(schema); + + expect(validator('option1').valid).toBe(true); + expect(validator(42).valid).toBe(true); + expect(validator(true).valid).toBe(true); + expect(validator(null).valid).toBe(true); + expect(validator('other').valid).toBe(false); + }); + }); + + describe('Object schemas', () => { + it('validates simple object', () => { + const schema: JsonSchemaType = { + type: 'object', + properties: { + name: { type: 'string' }, + age: { type: 'number' } + }, + required: ['name'] + }; + const validator = provider.getValidator(schema); + + expect(validator({ name: 'John', age: 30 }).valid).toBe(true); + expect(validator({ name: 'John' }).valid).toBe(true); + expect(validator({ age: 30 }).valid).toBe(false); + expect(validator({}).valid).toBe(false); + }); + + it('validates nested objects', () => { + const schema: JsonSchemaType = { + type: 'object', + properties: { + user: { + type: 'object', + properties: { + name: { type: 'string' }, + email: { type: 'string', format: 'email' } + }, + required: ['name'] + } + }, + required: ['user'] + }; + const validator = provider.getValidator(schema); + + expect( + validator({ + user: { name: 'John', email: 'john@example.com' } + }).valid + ).toBe(true); + + expect( + validator({ + user: { name: 'John' } + }).valid + ).toBe(true); + + expect( + validator({ + user: { email: 'john@example.com' } + }).valid + ).toBe(false); + }); + + it('validates object with additionalProperties: false', () => { + const schema: JsonSchemaType = { + type: 'object', + properties: { + name: { type: 'string' } + }, + additionalProperties: false + }; + const validator = provider.getValidator(schema); + + expect(validator({ name: 'John' }).valid).toBe(true); + expect(validator({ name: 'John', extra: 'field' }).valid).toBe(false); + }); + }); + + describe('Array schemas', () => { + it('validates array of strings', () => { + const schema: JsonSchemaType = { + type: 'array', + items: { type: 'string' } + }; + const validator = provider.getValidator(schema); + + expect(validator(['a', 'b', 'c']).valid).toBe(true); + expect(validator([]).valid).toBe(true); + expect(validator(['a', 1, 'c']).valid).toBe(false); + }); + + it('validates array length constraints', () => { + const schema: JsonSchemaType = { + type: 'array', + items: { type: 'number' }, + minItems: 1, + maxItems: 3 + }; + const validator = provider.getValidator(schema); + + expect(validator([1]).valid).toBe(true); + expect(validator([1, 2, 3]).valid).toBe(true); + expect(validator([]).valid).toBe(false); + expect(validator([1, 2, 3, 4]).valid).toBe(false); + }); + + it('validates array with unique items', () => { + const schema: JsonSchemaType = { + type: 'array', + items: { type: 'number' }, + uniqueItems: true + }; + const validator = provider.getValidator(schema); + + expect(validator([1, 2, 3]).valid).toBe(true); + expect(validator([1, 2, 2, 3]).valid).toBe(false); + }); + }); + + describe('JSON Schema 2020-12 features', () => { + it('validates schema with $schema field', () => { + const schema: JsonSchemaType = { + $schema: 'https://json-schema.org/draft/2020-12/schema', + type: 'string' + }; + const validator = provider.getValidator(schema); + + expect(validator('test').valid).toBe(true); + }); + + it('validates schema with $id field', () => { + const schema: JsonSchemaType = { + $id: 'https://example.com/schemas/test', + type: 'number' + }; + const validator = provider.getValidator(schema); + + expect(validator(42).valid).toBe(true); + }); + + it('validates with allOf', () => { + const schema: JsonSchemaType = { + allOf: [ + { type: 'object', properties: { name: { type: 'string' } } }, + { type: 'object', properties: { age: { type: 'number' } } } + ] + }; + const validator = provider.getValidator(schema); + + expect(validator({ name: 'John', age: 30 }).valid).toBe(true); + expect(validator({ name: 'John' }).valid).toBe(true); + expect(validator({ name: 123 }).valid).toBe(false); + }); + + it('validates with anyOf', () => { + const schema: JsonSchemaType = { + anyOf: [{ type: 'string' }, { type: 'number' }] + }; + const validator = provider.getValidator(schema); + + expect(validator('test').valid).toBe(true); + expect(validator(42).valid).toBe(true); + expect(validator(true).valid).toBe(false); + }); + + it('validates with oneOf', () => { + const schema: JsonSchemaType = { + oneOf: [ + { type: 'string', minLength: 5 }, + { type: 'string', maxLength: 3 } + ] + }; + const validator = provider.getValidator(schema); + + expect(validator('ab').valid).toBe(true); // Matches second only + expect(validator('hello').valid).toBe(true); // Matches first only + expect(validator('abcd').valid).toBe(false); // Matches neither + }); + + it('validates with not', () => { + const schema: JsonSchemaType = { + not: { type: 'null' } + }; + const validator = provider.getValidator(schema); + + expect(validator('test').valid).toBe(true); + expect(validator(42).valid).toBe(true); + expect(validator(null).valid).toBe(false); + }); + + it('validates with const', () => { + const schema: JsonSchemaType = { + const: 'specific-value' + }; + const validator = provider.getValidator(schema); + + expect(validator('specific-value').valid).toBe(true); + expect(validator('other-value').valid).toBe(false); + }); + }); + + describe('Complex real-world schemas', () => { + it('validates user registration form', () => { + const schema: JsonSchemaType = { + type: 'object', + properties: { + username: { + type: 'string', + minLength: 3, + maxLength: 20, + pattern: '^[a-zA-Z0-9_]+$' + }, + email: { + type: 'string', + format: 'email' + }, + age: { + type: 'integer', + minimum: 18, + maximum: 120 + }, + newsletter: { + type: 'boolean', + default: false + } + }, + required: ['username', 'email'] + }; + const validator = provider.getValidator(schema); + + expect( + validator({ + username: 'john_doe', + email: 'john@example.com', + age: 25, + newsletter: true + }).valid + ).toBe(true); + + expect( + validator({ + username: 'john_doe', + email: 'john@example.com' + }).valid + ).toBe(true); + + expect( + validator({ + username: 'ab', // Too short + email: 'john@example.com' + }).valid + ).toBe(false); + + expect( + validator({ + username: 'john_doe', + email: 'invalid-email' + }).valid + ).toBe(false); + }); + + it('validates API response with nested structure', () => { + const schema: JsonSchemaType = { + type: 'object', + properties: { + status: { + type: 'string', + enum: ['success', 'error', 'pending'] + }, + data: { + type: 'object', + properties: { + id: { type: 'string' }, + items: { + type: 'array', + items: { + type: 'object', + properties: { + name: { type: 'string' }, + quantity: { type: 'integer', minimum: 1 } + }, + required: ['name', 'quantity'] + } + } + }, + required: ['id', 'items'] + }, + timestamp: { + type: 'string', + format: 'date-time' + } + }, + required: ['status', 'data'] + }; + const validator = provider.getValidator(schema); + + expect( + validator({ + status: 'success', + data: { + id: '123', + items: [ + { name: 'Item 1', quantity: 5 }, + { name: 'Item 2', quantity: 3 } + ] + }, + timestamp: '2025-10-17T12:00:00Z' + }).valid + ).toBe(true); + + expect( + validator({ + status: 'invalid-status', + data: { id: '123', items: [] } + }).valid + ).toBe(false); + }); + }); + + describe('Error messages', () => { + it('provides helpful error message on validation failure', () => { + const schema: JsonSchemaType = { + type: 'object', + properties: { + name: { type: 'string' } + }, + required: ['name'] + }; + const validator = provider.getValidator(schema); + + const result = validator({}); + expect(result.valid).toBe(false); + expect(result.errorMessage).toBeDefined(); + expect(result.errorMessage).toBeTruthy(); + expect(typeof result.errorMessage).toBe('string'); + }); + }); + }); +}); + +describe('Missing dependencies', () => { + describe('AJV not installed but CfWorker is', () => { + beforeEach(() => { + vi.resetModules(); + }); + + afterEach(() => { + vi.doUnmock('ajv'); + vi.doUnmock('ajv-formats'); + }); + + it('should throw error when trying to import ajv-provider without ajv', async () => { + // Mock ajv as not installed + vi.doMock('ajv', () => { + throw new Error("Cannot find module 'ajv'"); + }); + + vi.doMock('ajv-formats', () => { + throw new Error("Cannot find module 'ajv-formats'"); + }); + + // Attempting to import ajv-provider should fail + await expect(import('../../src/validators/ajvProvider')).rejects.toThrow(); + }); + + it('should be able to import cfWorkerProvider when ajv is missing', async () => { + // Mock ajv as not installed + vi.doMock('ajv', () => { + throw new Error("Cannot find module 'ajv'"); + }); + + vi.doMock('ajv-formats', () => { + throw new Error("Cannot find module 'ajv-formats'"); + }); + + // But cfWorkerProvider should import successfully + const cfworkerModule = await import('../../src/validators/cfWorkerProvider'); + expect(cfworkerModule.CfWorkerJsonSchemaValidator).toBeDefined(); + + // And should work correctly + const validator = new cfworkerModule.CfWorkerJsonSchemaValidator(); + const schema: JsonSchemaType = { type: 'string' }; + const validatorFn = validator.getValidator(schema); + expect(validatorFn('test').valid).toBe(true); + }); + }); + + describe('CfWorker not installed but AJV is', () => { + beforeEach(() => { + vi.resetModules(); + }); + + afterEach(() => { + vi.doUnmock('@cfworker/json-schema'); + }); + + it('should throw error when trying to import cfWorkerProvider without @cfworker/json-schema', async () => { + // Mock @cfworker/json-schema as not installed + vi.doMock('@cfworker/json-schema', () => { + throw new Error("Cannot find module '@cfworker/json-schema'"); + }); + + // Attempting to import cfWorkerProvider should fail + await expect(import('../../src/validators/cfWorkerProvider')).rejects.toThrow(); + }); + + it('should be able to import ajv-provider when @cfworker/json-schema is missing', async () => { + // Mock @cfworker/json-schema as not installed + vi.doMock('@cfworker/json-schema', () => { + throw new Error("Cannot find module '@cfworker/json-schema'"); + }); + + // But ajv-provider should import successfully + const ajvModule = await import('../../src/validators/ajvProvider'); + expect(ajvModule.AjvJsonSchemaValidator).toBeDefined(); + + // And should work correctly + const validator = new ajvModule.AjvJsonSchemaValidator(); + const schema: JsonSchemaType = { type: 'string' }; + const validatorFn = validator.getValidator(schema); + expect(validatorFn('test').valid).toBe(true); + }); + + it('should document that @cfworker/json-schema is required', () => { + const cfworkerProviderPath = path.join(__dirname, '../../src/validators/cfWorkerProvider.ts'); + const content = readFileSync(cfworkerProviderPath, 'utf8'); + + expect(content).toContain('@cfworker/json-schema'); + }); + }); +}); diff --git a/packages/core-internal/tsconfig.json b/packages/core-internal/tsconfig.json new file mode 100644 index 0000000000..a6838303e4 --- /dev/null +++ b/packages/core-internal/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "@modelcontextprotocol/tsconfig", + "include": ["./"], + "exclude": ["node_modules", "dist"], + "compilerOptions": { + "paths": { + "*": ["./*"], + "@modelcontextprotocol/eslint-config": ["./node_modules/@modelcontextprotocol/eslint-config/tsconfig.json"], + "@modelcontextprotocol/vitest-config": ["./node_modules/@modelcontextprotocol/vitest-config/tsconfig.json"] + } + } +} diff --git a/packages/core-internal/vitest.config.js b/packages/core-internal/vitest.config.js new file mode 100644 index 0000000000..496fca3200 --- /dev/null +++ b/packages/core-internal/vitest.config.js @@ -0,0 +1,3 @@ +import baseConfig from '@modelcontextprotocol/vitest-config'; + +export default baseConfig; From 5621119a7b786f995d4ab6238eb98da7e93289a3 Mon Sep 17 00:00:00 2001 From: Konstantin Konstantinov Date: Thu, 25 Jun 2026 18:06:47 +0300 Subject: [PATCH 17/21] docs fix --- CLAUDE.md | 6 +++--- docs/migration-SKILL.md | 22 ++++++++++++---------- docs/migration.md | 22 +++++++++++++--------- typedoc.config.mjs | 15 ++++++--------- 4 files changed, 34 insertions(+), 31 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 64f3741920..7f43e843ef 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -68,12 +68,12 @@ The SDK has a two-layer export structure to separate internal code from the publ - **`@modelcontextprotocol/core-internal`** (main entry, `packages/core-internal/src/index.ts`) — Internal barrel. Exports everything (including Zod schemas, Protocol class, stdio utils). Only consumed by sibling packages within the monorepo (`private: true`). - **`@modelcontextprotocol/core-internal/public`** (`packages/core-internal/src/exports/public/index.ts`) — Curated public API. Exports only TypeScript types, error classes, constants, and guards. Re-exported by client and server packages. -- **`@modelcontextprotocol/client`** and **`@modelcontextprotocol/server`** (`packages/*/src/index.ts`) — Final public surface. Package-specific exports (named explicitly) plus re-exports from `core/public`. +- **`@modelcontextprotocol/client`** and **`@modelcontextprotocol/server`** (`packages/*/src/index.ts`) — Final public surface. Package-specific exports (named explicitly) plus re-exports from `core-internal/public`. When modifying exports: -- Use explicit named exports, not `export *`, in package `index.ts` files and `core/public`. +- Use explicit named exports, not `export *`, in package `index.ts` files and `core-internal/public`. - Adding a symbol to a package `index.ts` makes it public API — do so intentionally. -- Internal helpers should stay in the core internal barrel and not be added to `core/public` or package index files. +- Internal helpers should stay in the core internal barrel and not be added to `core-internal/public` or package index files. - The package root entry must stay runtime-neutral so browser and Cloudflare Workers bundlers can consume it. Exports whose module graph transitively touches unpolyfillable Node builtins (`node:child_process`, `node:net`, `cross-spawn`, etc.) must live at a named subpath export (e.g. `./stdio`) and be covered by a `barrelClean` test in that package. ### Transport System diff --git a/docs/migration-SKILL.md b/docs/migration-SKILL.md index 85ffb807e9..b9c87351d3 100644 --- a/docs/migration-SKILL.md +++ b/docs/migration-SKILL.md @@ -1,6 +1,6 @@ --- name: migrate-v1-to-v2 -description: Migrate MCP TypeScript SDK code from v1 (@modelcontextprotocol/sdk) to v2 (@modelcontextprotocol/core-internal, /client, /server). Use when a user asks to migrate, upgrade, or port their MCP TypeScript code from v1 to v2. +description: Migrate MCP TypeScript SDK code from v1 (@modelcontextprotocol/sdk) to v2 (@modelcontextprotocol/client, /server, /core). Use when a user asks to migrate, upgrade, or port their MCP TypeScript code from v1 to v2. --- # MCP TypeScript SDK: v1 → v2 Migration @@ -20,15 +20,17 @@ Remove the old package and install only what you need: npm uninstall @modelcontextprotocol/sdk ``` -| You need | Install | -| --------------------- | ------------------------------------------------------------------------ | -| Client only | `npm install @modelcontextprotocol/client` | -| Server only | `npm install @modelcontextprotocol/server` | -| Server + Node.js HTTP | `npm install @modelcontextprotocol/server @modelcontextprotocol/node` | -| Server + Express | `npm install @modelcontextprotocol/server @modelcontextprotocol/express` | -| Server + Hono | `npm install @modelcontextprotocol/server @modelcontextprotocol/hono` | - -`@modelcontextprotocol/core-internal` is installed automatically as a dependency. +| You need | Install | +| --------------------------- | ------------------------------------------------------------------------ | +| Client only | `npm install @modelcontextprotocol/client` | +| Server only | `npm install @modelcontextprotocol/server` | +| Server + Node.js HTTP | `npm install @modelcontextprotocol/server @modelcontextprotocol/node` | +| Server + Express | `npm install @modelcontextprotocol/server @modelcontextprotocol/express` | +| Server + Hono | `npm install @modelcontextprotocol/server @modelcontextprotocol/hono` | +| Raw Zod `*Schema` constants | `npm install @modelcontextprotocol/core` | + +`@modelcontextprotocol/client` and `@modelcontextprotocol/server` are self-contained — the shared types, protocol layer, and transports are bundled in and re-exported, so there is no separate runtime package to install for them. Install `@modelcontextprotocol/core` only if you +import the raw Zod `*Schema` constants directly (see §11). ## 3. Import Mapping diff --git a/docs/migration.md b/docs/migration.md index 16340df04b..260ec4f7fa 100644 --- a/docs/migration.md +++ b/docs/migration.md @@ -4,8 +4,8 @@ This guide covers the breaking changes introduced in v2 of the MCP TypeScript SD ## Overview -Version 2 of the MCP TypeScript SDK introduces several breaking changes to improve modularity, reduce dependency bloat, and provide a cleaner API surface. The biggest change is the split from a single `@modelcontextprotocol/sdk` package into separate `@modelcontextprotocol/core-internal`, -`@modelcontextprotocol/client`, and `@modelcontextprotocol/server` packages. +Version 2 of the MCP TypeScript SDK introduces several breaking changes to improve modularity, reduce dependency bloat, and provide a cleaner API surface. The biggest change is the split from a single `@modelcontextprotocol/sdk` package into focused `@modelcontextprotocol/client` +and `@modelcontextprotocol/server` packages, with the shared Zod schema constants published separately as `@modelcontextprotocol/core`. > **Formatting:** The `@modelcontextprotocol/codemod` package automates most of the mechanical changes below, but it rewrites your code's AST without reformatting it — wrapped schemas and generated handler method strings may not match your project's style. After migrating (with > the codemod or by hand), run your formatter on the changed files — for example `prettier --write`, `eslint --fix`, or `biome format --write` — and review the diff. @@ -14,13 +14,16 @@ Version 2 of the MCP TypeScript SDK introduces several breaking changes to impro ### Package split (monorepo) -The single `@modelcontextprotocol/sdk` package has been split into three packages: +The single `@modelcontextprotocol/sdk` package has been split into focused packages: -| v1 | v2 | -| --------------------------- | ---------------------------------------------------------- | -| `@modelcontextprotocol/sdk` | `@modelcontextprotocol/core-internal` (types, protocol, transports) | -| | `@modelcontextprotocol/client` (client implementation) | -| | `@modelcontextprotocol/server` (server implementation) | +| v1 | v2 | +| --------------------------- | ----------------------------------------------------------------------------- | +| `@modelcontextprotocol/sdk` | `@modelcontextprotocol/client` (client implementation) | +| | `@modelcontextprotocol/server` (server implementation) | +| | `@modelcontextprotocol/core` (Zod `*Schema` constants — install only if used) | + +`@modelcontextprotocol/client` and `@modelcontextprotocol/server` are self-contained: the shared protocol types, error classes, guards, and transports are bundled in and re-exported, so most projects install only one of them. `@modelcontextprotocol/core` is a separate, +lightweight package needed only if you import the raw Zod `*Schema` constants directly (see [Zod schema constants](#protocolrequest-ctxmcpreqsend-and-clientcalltool-no-longer-require-a-schema-parameter-for-spec-methods) below). Remove the old package and install only the packages you need: @@ -33,7 +36,8 @@ npm install @modelcontextprotocol/client # If you only need a server npm install @modelcontextprotocol/server -# Both packages depend on @modelcontextprotocol/core-internal automatically +# Only if you import the raw Zod *Schema constants directly +npm install @modelcontextprotocol/core ``` Update your imports accordingly: diff --git a/typedoc.config.mjs b/typedoc.config.mjs index 4e3ee285fe..5835ac2a63 100644 --- a/typedoc.config.mjs +++ b/typedoc.config.mjs @@ -16,7 +16,12 @@ const packages = packageJsonPaths.map(p => { return { rootDir, manifest }; }); -const publicPackages = packages.filter(p => p.manifest.private !== true); +// @modelcontextprotocol/core is published for direct schema imports (CallToolResultSchema.parse(...)), +// but it's a thin re-export of the spec/OAuth Zod schemas whose JSDoc cross-references TYPES that live +// in client/server — unresolvable from core's own per-package doc scope. We skip rendering its API docs +// (the schemas mirror the documented types 1:1) so monorepo-wide invalid-link validation can stay ON. +const DOCS_EXCLUDED_PACKAGES = new Set(['@modelcontextprotocol/core']); +const publicPackages = packages.filter(p => p.manifest.private !== true && !DOCS_EXCLUDED_PACKAGES.has(p.manifest.name)); const entryPoints = publicPackages.map(p => p.rootDir); console.log( @@ -47,14 +52,6 @@ export default { readme: false }, customJs: 'docs/v2-banner.js', - // The spec-generated schema/type JSDoc uses `{@linkcode | method}` cross-references. - // With the data model split across packages (Zod schemas in @modelcontextprotocol/core, - // their types in @modelcontextprotocol/server / -client), typedoc's per-package link resolution - // can't resolve those bare cross-package references. Disable only the invalid-link check; every - // other validation (notExported, etc.) stays on under treatWarningsAsErrors. - validation: { - invalidLink: false - }, treatWarningsAsErrors: true, out: 'tmp/docs/', externalSymbolLinkMappings: { From 51bf8bc78b29feb1e7fc08cb0e2f052bb3395341 Mon Sep 17 00:00:00 2001 From: Konstantin Konstantinov Date: Thu, 25 Jun 2026 18:31:28 +0300 Subject: [PATCH 18/21] fixes --- docs/migration-SKILL.md | 1 + docs/migration.md | 3 + .../migrations/v1-to-v2/mappings/symbolMap.ts | 14 +- .../v1-to-v2/transforms/mockPaths.ts | 150 ++++++++++++++---- packages/codemod/test/detectFormatter.test.ts | 2 +- packages/codemod/test/projectAnalyzer.test.ts | 4 +- .../test/v1-to-v2/authSchemaNames.test.ts | 2 +- .../test/v1-to-v2/specSchemaNames.test.ts | 2 +- .../v1-to-v2/transforms/mockPaths.test.ts | 89 +++++++++++ .../v1-to-v2/transforms/symbolRenames.test.ts | 15 ++ packages/core/test/coreSchemas.test.ts | 4 +- scripts/fetch-spec-types.ts | 2 +- 12 files changed, 247 insertions(+), 41 deletions(-) diff --git a/docs/migration-SKILL.md b/docs/migration-SKILL.md index b9c87351d3..73a2e4a0b9 100644 --- a/docs/migration-SKILL.md +++ b/docs/migration-SKILL.md @@ -90,6 +90,7 @@ Notes: | `isJSONRPCError` | `isJSONRPCErrorResponse` | | `isJSONRPCResponse` (deprecated in v1) | `isJSONRPCResultResponse` (**not** v2's new `isJSONRPCResponse`, which correctly matches both result and error) | | `JSONRPCResponseSchema` (result-only in v1) | `JSONRPCResultResponseSchema` (from `@modelcontextprotocol/core`; **not** v2's new `JSONRPCResponseSchema`, a `z.union` that also accepts error responses) | +| `JSONRPCResponse` (type, result-only in v1) | `JSONRPCResultResponse` (**not** v2's new `JSONRPCResponse` type, which is the result\|error union) | | `ResourceReference` | `ResourceTemplateReference` | | `ResourceReferenceSchema` | `ResourceTemplateReferenceSchema` | | `IsomorphicHeaders` | REMOVED (use Web Standard `Headers`) | diff --git a/docs/migration.md b/docs/migration.md index 260ec4f7fa..1b6062225c 100644 --- a/docs/migration.md +++ b/docs/migration.md @@ -610,6 +610,9 @@ All other symbols exported from `@modelcontextprotocol/sdk/types.js` retain thei > **Note on `JSONRPCResponseSchema`:** the Zod schema follows the same pattern. v1's `JSONRPCResponseSchema` validated only _result_ responses; v2 reuses the name for a `z.union([JSONRPCResultResponseSchema, JSONRPCErrorResponseSchema])` that also accepts error responses. If you > are migrating v1 code that called `JSONRPCResponseSchema.parse()`/`.safeParse()`, rename it to `JSONRPCResultResponseSchema` (re-exported by `@modelcontextprotocol/core`) to preserve the original validation. The codemod performs this rename automatically. +> **Note on the `JSONRPCResponse` type:** the TypeScript type follows the same pattern. v1's `JSONRPCResponse` was the _result-only_ response type; v2 reuses the name for the result\|error union (`Infer`). If you are migrating v1 code that annotated values with +> `JSONRPCResponse`, rename it to `JSONRPCResultResponse` (re-exported by `@modelcontextprotocol/client` and `@modelcontextprotocol/server`) to preserve the original narrower type. The codemod performs this rename automatically. + **Before (v1):** ```typescript diff --git a/packages/codemod/src/migrations/v1-to-v2/mappings/symbolMap.ts b/packages/codemod/src/migrations/v1-to-v2/mappings/symbolMap.ts index d7220da548..b2670dd097 100644 --- a/packages/codemod/src/migrations/v1-to-v2/mappings/symbolMap.ts +++ b/packages/codemod/src/migrations/v1-to-v2/mappings/symbolMap.ts @@ -4,12 +4,14 @@ export const SIMPLE_RENAMES: Record = { JSONRPCErrorSchema: 'JSONRPCErrorResponseSchema', isJSONRPCError: 'isJSONRPCErrorResponse', isJSONRPCResponse: 'isJSONRPCResultResponse', - // v1's JSONRPCResponseSchema validated only *result* responses. v2 reuses the name for a - // z.union([JSONRPCResultResponseSchema, JSONRPCErrorResponseSchema]) that also accepts error - // responses, so a migrated `JSONRPCResponseSchema.parse(...)` would silently widen. Rename to the - // result-only schema to preserve v1 behavior — mirroring the isJSONRPCResponse guard rename above. - // (The TYPE JSONRPCResponse/JSONRPCResultResponse is not part of the public v2 surface, so only the - // schema constant — re-exported by core — is renamed here.) + // v1's JSONRPCResponse type / JSONRPCResponseSchema constant both validated only *result* + // responses. v2 reuses each name for the result|error form — the type becomes + // `Infer` and the schema the + // matching `z.union([...])` — so a migrated `JSONRPCResponseSchema.parse(...)` (or a typed + // `JSONRPCResponse` value) would silently widen. Rename both to the result-only equivalents to + // preserve v1 behavior — mirroring the isJSONRPCResponse guard rename above. Both the type and the + // schema are public in v2 (re-exported from core via @modelcontextprotocol/client | /server). + JSONRPCResponse: 'JSONRPCResultResponse', JSONRPCResponseSchema: 'JSONRPCResultResponseSchema', ResourceReference: 'ResourceTemplateReference', ResourceReferenceSchema: 'ResourceTemplateReferenceSchema' diff --git a/packages/codemod/src/migrations/v1-to-v2/transforms/mockPaths.ts b/packages/codemod/src/migrations/v1-to-v2/transforms/mockPaths.ts index 78e77aad03..c370ba4fce 100644 --- a/packages/codemod/src/migrations/v1-to-v2/transforms/mockPaths.ts +++ b/packages/codemod/src/migrations/v1-to-v2/transforms/mockPaths.ts @@ -7,7 +7,7 @@ import { isSdkSpecifier } from '../../../utils/importUtils'; import { resolveTypesPackage } from '../../../utils/projectAnalyzer'; import type { ImportMapping } from '../mappings/importMap'; import { isAuthImport, lookupImportMapping } from '../mappings/importMap'; -import { symbolTargetOverride } from '../mappings/schemaRouting'; +import { isSharedSchemaConst, resolveRenamedName, symbolTargetOverride } from '../mappings/schemaRouting'; import { SIMPLE_RENAMES } from '../mappings/symbolMap'; /** @@ -73,6 +73,7 @@ function resolveTarget( specifier: string, context: TransformContext, sourceFile: SourceFile, + symbols: string[], diagnosticSink?: { filePath: string; line: number; diagnostics: Diagnostic[] } ): { target: string; mapping: ImportMapping } | { removed: true; isV2Gap?: boolean; removalMessage?: string } | null { const mapping = lookupImportMapping(specifier); @@ -93,7 +94,15 @@ function resolveTarget( const s = i.getModuleSpecifierValue(); return s.includes('/server/') || s === '@modelcontextprotocol/server'; }); - target = resolveTypesPackage(context, hasClient, hasServer, diagnosticSink); + // Resolve lazily: only pass the diagnostic sink to resolveTypesPackage when the routed target + // actually falls back to the context package. A factory/destructuring whose symbols all route + // elsewhere (e.g. only `*Schema` constants → core) never uses the context package, so emitting a + // "could not determine project type" warning (or a 'both'-project info note) for it would be + // spurious. Mirrors the lazy `needsContext` guard in the static import transform. A + // non-destructured/non-routable binding has no symbols, so `routeSymbols` returns no target and + // context is (correctly) treated as needed. + const needsContext = routeSymbols(symbols, mapping).target === undefined; + target = resolveTypesPackage(context, hasClient, hasServer, needsContext ? diagnosticSink : undefined); if (mapping.subpathSuffix) { target = `${target}${mapping.subpathSuffix}`; } @@ -121,7 +130,8 @@ function rewriteMockCall( const specifier = firstArg.getLiteralValue(); if (!isSdkSpecifier(specifier)) return 0; - const resolved = resolveTarget(specifier, context, sourceFile, { + const factorySymbols = args.length >= 2 ? collectFactorySymbols(args[1]!) : []; + const resolved = resolveTarget(specifier, context, sourceFile, factorySymbols, { filePath: sourceFile.getFilePath(), line: call.getStartLineNumber(), diagnostics @@ -150,7 +160,7 @@ function rewriteMockCall( // only `*Schema` constants (from sdk/types.js or sdk/shared/auth.js) moves to core; a factory // of only `StreamableHTTPServerTransport` moves to @modelcontextprotocol/node. A single mock path // can't be split, so a mix of packages is flagged for manual migration. - const { target: routedTarget, mixed } = routeSymbols(collectFactorySymbols(args[1]!), resolved.mapping); + const { target: routedTarget, mixed } = routeSymbols(factorySymbols, resolved.mapping); if (routedTarget) { effectiveTarget = routedTarget; } else if (mixed) { @@ -241,6 +251,80 @@ function renameSymbolsInFactory(factoryArg: import('ts-morph').Node, renamedSymb return changes; } +/** + * The destructured binding keys of an `await import()` assigned to an object binding pattern (e.g. + * `const { CallToolResultSchema, McpError } = await import('…')`), or `[]` for a non-destructured + * binding, a `.then()` chain, or an unassigned `await import()`. The keys feed per-symbol routing — + * the specifier itself can't be split. + */ +function getDestructuredKeys(node: import('ts-morph').CallExpression): string[] { + const parent = node.getParent(); + if (!parent || !Node.isAwaitExpression(parent)) return []; + const grandParent = parent.getParent(); + if (!grandParent || !Node.isVariableDeclaration(grandParent)) return []; + const nameNode = grandParent.getNameNode(); + if (!Node.isObjectBindingPattern(nameNode)) return []; + return nameNode.getElements().map(el => el.getPropertyNameNode()?.getText() ?? el.getName()); +} + +/** + * For a dynamic import whose module binding is NOT a destructurable object pattern — a non-destructured + * `const mod = await import('…')` or a `.then(m => …)` chain — collect the Zod schema constants + * accessed off that binding (e.g. `mod.OAuthTokensSchema`). The destructured form is routed/renamed + * elsewhere; these forms can't be split per-symbol, so the schema accesses are surfaced as a diagnostic + * (mirroring the namespace-import branch of the static import transform — see `importPaths.ts`). Returns + * deduped `[v1Name, v2Name]` pairs (a schema may be renamed, e.g. JSONRPCResponseSchema → + * JSONRPCResultResponseSchema, and core only exports the v2 name). Empty unless the mapping carries a + * `schemaSymbolTarget`. + */ +function collectModuleSchemaAccesses( + node: import('ts-morph').CallExpression, + mapping: ImportMapping, + sourceFile: SourceFile +): Array { + if (!mapping.schemaSymbolTarget) return []; + + let bindingName: string | undefined; + let scope: import('ts-morph').Node | undefined; + + const parent = node.getParent(); + if (parent && Node.isAwaitExpression(parent)) { + // const mod = await import('…') → `mod` is in scope for the rest of the file. + const grandParent = parent.getParent(); + if (grandParent && Node.isVariableDeclaration(grandParent)) { + const nameNode = grandParent.getNameNode(); + if (Node.isIdentifier(nameNode)) { + bindingName = nameNode.getText(); + scope = sourceFile; + } + } + } else if (parent && Node.isPropertyAccessExpression(parent) && parent.getName() === 'then') { + // import('…').then(m => m.XxxSchema…) → the module is the `.then` callback's first parameter. + const thenCall = parent.getParent(); + if (thenCall && Node.isCallExpression(thenCall)) { + const cb = thenCall.getArguments()[0]; + if (cb && (Node.isArrowFunction(cb) || Node.isFunctionExpression(cb))) { + const paramName = cb.getParameters()[0]?.getNameNode(); + if (paramName && Node.isIdentifier(paramName)) { + bindingName = paramName.getText(); + scope = cb; + } + } + } + } + + if (!bindingName || !scope) return []; + + return [ + ...new Map( + scope + .getDescendantsOfKind(SyntaxKind.PropertyAccessExpression) + .filter(pa => pa.getExpression().getText() === bindingName && isSharedSchemaConst(pa.getName(), mapping)) + .map(pa => [pa.getName(), resolveRenamedName(pa.getName(), mapping)] as const) + ) + ]; +} + function rewriteDynamicImports( sourceFile: SourceFile, context: TransformContext, @@ -264,7 +348,8 @@ function rewriteDynamicImports( const specifier = firstArg.getLiteralValue(); if (!isSdkSpecifier(specifier)) return; - const resolved = resolveTarget(specifier, context, sourceFile, { + const destructuredKeys = getDestructuredKeys(node); + const resolved = resolveTarget(specifier, context, sourceFile, destructuredKeys, { filePath: sourceFile.getFilePath(), line: node.getStartLineNumber(), diagnostics @@ -294,28 +379,39 @@ function rewriteDynamicImports( // of only `*Schema` constants (e.g. `const { CallToolResultSchema } = await import('…/types.js')`) // moves to core, and `StreamableHTTPServerTransport` moves to @modelcontextprotocol/node. A // single import() specifier can't be split, so a mix of packages is flagged for manual migration. - const parentExpr = node.getParent(); - if (parentExpr && Node.isAwaitExpression(parentExpr)) { - const grandParent = parentExpr.getParent(); - if (grandParent && Node.isVariableDeclaration(grandParent)) { - const nameNode = grandParent.getNameNode(); - if (Node.isObjectBindingPattern(nameNode)) { - const keys = nameNode.getElements().map(el => el.getPropertyNameNode()?.getText() ?? el.getName()); - const { target: routedTarget, mixed } = routeSymbols(keys, resolved.mapping); - if (routedTarget) { - effectiveTarget = routedTarget; - } else if (mixed) { - diagnostics.push( - actionRequired( - sourceFile.getFilePath(), - node, - `Dynamic import of ${specifier} destructures symbols that belong to different v2 packages. ` + - `Split the import manually so each symbol targets the correct package.` - ) - ); - } - } - } + const { target: routedTarget, mixed } = routeSymbols(destructuredKeys, resolved.mapping); + if (routedTarget) { + effectiveTarget = routedTarget; + } else if (mixed) { + diagnostics.push( + actionRequired( + sourceFile.getFilePath(), + node, + `Dynamic import of ${specifier} destructures symbols that belong to different v2 packages. ` + + `Split the import manually so each symbol targets the correct package.` + ) + ); + } + + // A non-destructured binding (`const mod = await import('…')`) or a `.then(m => …)` chain can't be + // routed per-symbol, so the specifier moves to the context package — which does NOT export the + // Zod `*Schema` constants (those live in `schemaSymbolTarget`/core). Any `mod.Schema` / + // `m.Schema` accesses would silently break, so flag them (mirroring the namespace-import + // branch of the static import transform). The destructured form is handled by `routeSymbols` above. + const schemaAccesses = collectModuleSchemaAccesses(node, resolved.mapping, sourceFile); + if (schemaAccesses.length > 0) { + const accessed = schemaAccesses.map(([v1]) => v1).join(', '); + const importName = schemaAccesses[0]![1]; + const renamed = schemaAccesses.filter(([v1, v2]) => v1 !== v2); + const renameNote = renamed.length > 0 ? ` Renamed in v2: ${renamed.map(([v1, v2]) => `${v1} → ${v2}`).join(', ')}.` : ''; + diagnostics.push( + actionRequired( + sourceFile.getFilePath(), + node, + `Dynamic import of ${specifier} is used to access Zod schema(s) (${accessed}) that moved to ${resolved.mapping.schemaSymbolTarget}.${renameNote} ` + + `Import them with a named import (e.g. \`import { ${importName} } from '${resolved.mapping.schemaSymbolTarget}'\`) and update the qualified usages.` + ) + ); } usedPackages.add(effectiveTarget); diff --git a/packages/codemod/test/detectFormatter.test.ts b/packages/codemod/test/detectFormatter.test.ts index 0b569a5e80..493cc3b574 100644 --- a/packages/codemod/test/detectFormatter.test.ts +++ b/packages/codemod/test/detectFormatter.test.ts @@ -3,7 +3,7 @@ import { tmpdir } from 'node:os'; import path from 'node:path'; import { describe, it, expect, afterEach } from 'vitest'; -import { detectFormatter } from '../src/utils/detectFormatter.js'; +import { detectFormatter } from '../src/utils/detectFormatter'; let tempDir: string; diff --git a/packages/codemod/test/projectAnalyzer.test.ts b/packages/codemod/test/projectAnalyzer.test.ts index 3906ddd922..222894aef7 100644 --- a/packages/codemod/test/projectAnalyzer.test.ts +++ b/packages/codemod/test/projectAnalyzer.test.ts @@ -202,7 +202,7 @@ describe('analyzeProject', () => { describe('resolveTypesPackage', () => { it('emits an info note (not a warning) for a both-project ambiguous file', () => { - const sink = { filePath: 'f.ts', line: 1, diagnostics: [] as import('../src/types.js').Diagnostic[] }; + const sink = { filePath: 'f.ts', line: 1, diagnostics: [] as import('../src/types').Diagnostic[] }; const target = resolveTypesPackage({ projectType: 'both' }, false, false, sink); expect(target).toBe('@modelcontextprotocol/server'); expect(sink.diagnostics).toHaveLength(1); @@ -210,7 +210,7 @@ describe('resolveTypesPackage', () => { }); it('emits an action-required warning for a genuinely unknown project', () => { - const sink = { filePath: 'f.ts', line: 1, diagnostics: [] as import('../src/types.js').Diagnostic[] }; + const sink = { filePath: 'f.ts', line: 1, diagnostics: [] as import('../src/types').Diagnostic[] }; resolveTypesPackage({ projectType: 'unknown' }, false, false, sink); expect(sink.diagnostics).toHaveLength(1); expect(sink.diagnostics[0]!.level).toBe('warning'); diff --git a/packages/codemod/test/v1-to-v2/authSchemaNames.test.ts b/packages/codemod/test/v1-to-v2/authSchemaNames.test.ts index 6e500d9b3f..6269c15573 100644 --- a/packages/codemod/test/v1-to-v2/authSchemaNames.test.ts +++ b/packages/codemod/test/v1-to-v2/authSchemaNames.test.ts @@ -3,7 +3,7 @@ import { fileURLToPath } from 'node:url'; import { describe, expect, it } from 'vitest'; -import { AUTH_SCHEMA_NAMES, AUTH_SCHEMA_NAMES_NO_V2_PUBLIC_EXPORT } from '../../src/migrations/v1-to-v2/mappings/authSchemaNames.js'; +import { AUTH_SCHEMA_NAMES, AUTH_SCHEMA_NAMES_NO_V2_PUBLIC_EXPORT } from '../../src/migrations/v1-to-v2/mappings/authSchemaNames'; describe('AUTH_SCHEMA_NAMES (codemod auth schema-routing allowlist)', () => { it('routes only auth schemas that @modelcontextprotocol/core exports (drift guard)', () => { diff --git a/packages/codemod/test/v1-to-v2/specSchemaNames.test.ts b/packages/codemod/test/v1-to-v2/specSchemaNames.test.ts index 73ca0d0a66..d7f6bf3dd8 100644 --- a/packages/codemod/test/v1-to-v2/specSchemaNames.test.ts +++ b/packages/codemod/test/v1-to-v2/specSchemaNames.test.ts @@ -3,7 +3,7 @@ import { fileURLToPath } from 'node:url'; import { describe, expect, it } from 'vitest'; -import { SPEC_SCHEMA_NAMES } from '../../src/migrations/v1-to-v2/mappings/specSchemaNames.js'; +import { SPEC_SCHEMA_NAMES } from '../../src/migrations/v1-to-v2/mappings/specSchemaNames'; describe('SPEC_SCHEMA_NAMES (codemod schema-routing allowlist)', () => { it("matches @modelcontextprotocol/core's exported schema set exactly (drift guard)", () => { diff --git a/packages/codemod/test/v1-to-v2/transforms/mockPaths.test.ts b/packages/codemod/test/v1-to-v2/transforms/mockPaths.test.ts index 40e8c577a2..f00f921b99 100644 --- a/packages/codemod/test/v1-to-v2/transforms/mockPaths.test.ts +++ b/packages/codemod/test/v1-to-v2/transforms/mockPaths.test.ts @@ -402,6 +402,95 @@ describe('mock-paths transform', () => { }); }); + describe('lazy context resolution (no spurious project-type diagnostic)', () => { + it('does not warn about project type for a schema-only vi.mock factory (unknown project)', () => { + // The factory routes entirely to core, so the context package is never used — resolveTypesPackage + // must not emit a "could not determine project type" warning. + const input = [`vi.mock('@modelcontextprotocol/sdk/types.js', () => ({`, ` CallToolResultSchema: vi.fn()`, `}));`, ''].join( + '\n' + ); + const project = new Project({ useInMemoryFileSystem: true }); + const sourceFile = project.createSourceFile('test.ts', input); + const result = mockPathsTransform.apply(sourceFile, { projectType: 'unknown' }); + expect(result.diagnostics.some(d => /determine project type/i.test(d.message))).toBe(false); + expect(sourceFile.getFullText()).toContain('@modelcontextprotocol/core'); + }); + + it('does not emit a both-project note for a schema-only destructured dynamic import (both project)', () => { + const input = [`const { OAuthTokensSchema } = await import('@modelcontextprotocol/sdk/shared/auth.js');`, ''].join('\n'); + const project = new Project({ useInMemoryFileSystem: true }); + const sourceFile = project.createSourceFile('test.ts', input); + const result = mockPathsTransform.apply(sourceFile, { projectType: 'both' }); + expect(result.diagnostics.some(d => /both client and server|determine project type/i.test(d.message))).toBe(false); + expect(sourceFile.getFullText()).toContain('@modelcontextprotocol/core'); + }); + + it('still warns about project type for a non-schema vi.mock factory (unknown project)', () => { + // Control: isInitializeRequest is a guard (not a schema constant), so the factory falls through to + // context resolution — the warning must still fire (lazy resolution must not suppress real fall-throughs). + const input = [`vi.mock('@modelcontextprotocol/sdk/types.js', () => ({`, ` isInitializeRequest: vi.fn()`, `}));`, ''].join( + '\n' + ); + const project = new Project({ useInMemoryFileSystem: true }); + const sourceFile = project.createSourceFile('test.ts', input); + const result = mockPathsTransform.apply(sourceFile, { projectType: 'unknown' }); + expect(result.diagnostics.some(d => /determine project type/i.test(d.message))).toBe(true); + }); + }); + + describe('non-destructured / .then dynamic import schema access (schemaSymbolTarget)', () => { + it('flags schema access on a non-destructured awaited dynamic import (types.js)', () => { + const input = [ + `const mod = await import('@modelcontextprotocol/sdk/types.js');`, + `const r = mod.CallToolResultSchema.parse(value);`, + '' + ].join('\n'); + const project = new Project({ useInMemoryFileSystem: true }); + const sourceFile = project.createSourceFile('test.ts', input); + const result = mockPathsTransform.apply(sourceFile, ctx); + expect( + result.diagnostics.some(d => d.message.includes('@modelcontextprotocol/core') && d.message.includes('CallToolResultSchema')) + ).toBe(true); + }); + + it('flags schema access in a .then() chain (shared/auth.js)', () => { + const input = [`import('@modelcontextprotocol/sdk/shared/auth.js').then(m => m.OAuthTokensSchema.parse(value));`, ''].join( + '\n' + ); + const project = new Project({ useInMemoryFileSystem: true }); + const sourceFile = project.createSourceFile('test.ts', input); + const result = mockPathsTransform.apply(sourceFile, ctx); + expect( + result.diagnostics.some(d => d.message.includes('@modelcontextprotocol/core') && d.message.includes('OAuthTokensSchema')) + ).toBe(true); + }); + + it('notes the rename for a renamed schema accessed in a .then() chain', () => { + const input = [`import('@modelcontextprotocol/sdk/types.js').then(m => m.JSONRPCResponseSchema.parse(value));`, ''].join('\n'); + const project = new Project({ useInMemoryFileSystem: true }); + const sourceFile = project.createSourceFile('test.ts', input); + const result = mockPathsTransform.apply(sourceFile, ctx); + expect( + result.diagnostics.some( + d => d.message.includes('JSONRPCResponseSchema') && d.message.includes('JSONRPCResultResponseSchema') + ) + ).toBe(true); + }); + + it('does not flag a non-destructured dynamic import with no schema access', () => { + // Control: `mod` is only used for a guard (not a schema constant), so no schema-moved-to-core note. + const input = [ + `const mod = await import('@modelcontextprotocol/sdk/types.js');`, + `const ok = mod.isInitializeRequest(value);`, + '' + ].join('\n'); + const project = new Project({ useInMemoryFileSystem: true }); + const sourceFile = project.createSourceFile('test.ts', input); + const result = mockPathsTransform.apply(sourceFile, ctx); + expect(result.diagnostics.some(d => d.message.includes('moved to @modelcontextprotocol/core'))).toBe(false); + }); + }); + describe('validator subpath rewrites', () => { it('rewrites vi.mock of validator provider to the subpath', () => { const input = [ diff --git a/packages/codemod/test/v1-to-v2/transforms/symbolRenames.test.ts b/packages/codemod/test/v1-to-v2/transforms/symbolRenames.test.ts index 9e3b1582ef..b9cb6137cb 100644 --- a/packages/codemod/test/v1-to-v2/transforms/symbolRenames.test.ts +++ b/packages/codemod/test/v1-to-v2/transforms/symbolRenames.test.ts @@ -57,6 +57,21 @@ describe('symbol-renames transform', () => { expect(result).not.toMatch(/(? { + // v1's JSONRPCResponse type was the result-only response; v2 reuses the name for a + // result|error union (Infer). Leaving the type unrenamed would + // silently widen a migrated v1 type import — mirror the schema/guard renames. + const input = [ + `import type { JSONRPCResponse } from '@modelcontextprotocol/server';`, + `function handle(r: JSONRPCResponse) { return r; }`, + '' + ].join('\n'); + const result = applyTransform(input); + expect(result).toContain('import type { JSONRPCResultResponse }'); + expect(result).toContain('r: JSONRPCResultResponse'); + expect(result).not.toMatch(/(? { const input = [ `import { ResourceReference } from '@modelcontextprotocol/sdk/types.js';`, diff --git a/packages/core/test/coreSchemas.test.ts b/packages/core/test/coreSchemas.test.ts index 3f57ae6e2b..3571989c3b 100644 --- a/packages/core/test/coreSchemas.test.ts +++ b/packages/core/test/coreSchemas.test.ts @@ -3,8 +3,8 @@ import { fileURLToPath } from 'node:url'; import { describe, expect, it } from 'vitest'; -import * as sdkShared from '../src/index.js'; -import { CursorSchema, InitializeRequestSchema, OAuthTokensSchema } from '../src/index.js'; +import * as sdkShared from '../src/index'; +import { CursorSchema, InitializeRequestSchema, OAuthTokensSchema } from '../src/index'; function readCore(relativePath: string): string { return readFileSync(fileURLToPath(new URL(relativePath, import.meta.url)), 'utf8'); diff --git a/scripts/fetch-spec-types.ts b/scripts/fetch-spec-types.ts index 90921276f2..568cd34263 100644 --- a/scripts/fetch-spec-types.ts +++ b/scripts/fetch-spec-types.ts @@ -90,7 +90,7 @@ async function updateSpecTypes(version: SpecVersion, providedSHA?: string): Prom const fullContent = header + specContent; // Format with prettier using the project's config so the output passes lint - const outputPath = join(PROJECT_ROOT, 'packages', 'core', 'src', 'types', `spec.types.${version}.ts`); + const outputPath = join(PROJECT_ROOT, 'packages', 'core-internal', 'src', 'types', `spec.types.${version}.ts`); const prettierConfig = await prettier.resolveConfig(outputPath); const formatted = await prettier.format(fullContent, { ...prettierConfig, filepath: outputPath }); From 3d7f4de07361d8a604de6b5fb59c8ec43363ae97 Mon Sep 17 00:00:00 2001 From: Konstantin Konstantinov Date: Thu, 25 Jun 2026 18:55:53 +0300 Subject: [PATCH 19/21] docs+lint: document public core package; drop stale core token from middleware eslint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Follow-ups to the core→core-internal / sdk-shared→core rename: - CLAUDE.md exports section was missing the public @modelcontextprotocol/core package - middleware express/fastify/hono internal-regex still listed a vestigial 'core' the sweep could not reach (non-contiguous substring inside the alternation) --- CLAUDE.md | 3 ++- packages/middleware/express/eslint.config.mjs | 2 +- packages/middleware/fastify/eslint.config.mjs | 2 +- packages/middleware/hono/eslint.config.mjs | 2 +- 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 7f43e843ef..9d792c3280 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -64,11 +64,12 @@ The SDK is organized into three main layers: ### Public API Exports -The SDK has a two-layer export structure to separate internal code from the public API: +The SDK separates internal code from the public API surface: - **`@modelcontextprotocol/core-internal`** (main entry, `packages/core-internal/src/index.ts`) — Internal barrel. Exports everything (including Zod schemas, Protocol class, stdio utils). Only consumed by sibling packages within the monorepo (`private: true`). - **`@modelcontextprotocol/core-internal/public`** (`packages/core-internal/src/exports/public/index.ts`) — Curated public API. Exports only TypeScript types, error classes, constants, and guards. Re-exported by client and server packages. - **`@modelcontextprotocol/client`** and **`@modelcontextprotocol/server`** (`packages/*/src/index.ts`) — Final public surface. Package-specific exports (named explicitly) plus re-exports from `core-internal/public`. +- **`@modelcontextprotocol/core`** (`packages/core/src/index.ts`) — Public Zod-schema package. Re-exports **only** the `*Schema` Zod constants (MCP spec + OAuth/OpenID), bundled from `core-internal` at build time. The published home for raw runtime validation (`CallToolResultSchema.parse(...)`); runtime-neutral (`zod` is its only dependency). Not consumed by the sibling packages — `client`/`server` keep their own bundled schema copies and stay Zod-free in their public surface. When modifying exports: - Use explicit named exports, not `export *`, in package `index.ts` files and `core-internal/public`. diff --git a/packages/middleware/express/eslint.config.mjs b/packages/middleware/express/eslint.config.mjs index 03d5331344..2284c163a1 100644 --- a/packages/middleware/express/eslint.config.mjs +++ b/packages/middleware/express/eslint.config.mjs @@ -6,7 +6,7 @@ export default [ ...baseConfig, { settings: { - 'import/internal-regex': '^@modelcontextprotocol/(server|core)' + 'import/internal-regex': '^@modelcontextprotocol/server' } } ]; diff --git a/packages/middleware/fastify/eslint.config.mjs b/packages/middleware/fastify/eslint.config.mjs index 03d5331344..2284c163a1 100644 --- a/packages/middleware/fastify/eslint.config.mjs +++ b/packages/middleware/fastify/eslint.config.mjs @@ -6,7 +6,7 @@ export default [ ...baseConfig, { settings: { - 'import/internal-regex': '^@modelcontextprotocol/(server|core)' + 'import/internal-regex': '^@modelcontextprotocol/server' } } ]; diff --git a/packages/middleware/hono/eslint.config.mjs b/packages/middleware/hono/eslint.config.mjs index 03d5331344..2284c163a1 100644 --- a/packages/middleware/hono/eslint.config.mjs +++ b/packages/middleware/hono/eslint.config.mjs @@ -6,7 +6,7 @@ export default [ ...baseConfig, { settings: { - 'import/internal-regex': '^@modelcontextprotocol/(server|core)' + 'import/internal-regex': '^@modelcontextprotocol/server' } } ]; From 0918528dd314596e02e8c567b66669e71f6d6391 Mon Sep 17 00:00:00 2001 From: Konstantin Konstantinov Date: Thu, 25 Jun 2026 19:40:12 +0300 Subject: [PATCH 20/21] docs+chore: fix stale rename prose in core; drop never-needed sdk-shared rename changeset MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - core index.ts/tsdown.config.ts/test comments pointed at core/src/... paths that now live under core-internal/src/...; renamed sdkSharedSchemas→coreSchemas refs - codemod drift-guard tests carried stale sdkShared* variable names - @modelcontextprotocol/sdk-shared was never published to npm, so the rename changeset's 'migrate your imports' entry would mislead the first core changelog; add-core-public-package.md already provides a single accurate 'new package' entry --- .changeset/rename-sdk-shared-to-core.md | 5 ---- .../test/v1-to-v2/authSchemaNames.test.ts | 8 +++--- .../test/v1-to-v2/specSchemaNames.test.ts | 6 ++--- packages/core/src/index.ts | 26 +++++++++---------- packages/core/test/coreSchemas.test.ts | 16 ++++++------ packages/core/tsdown.config.ts | 8 +++--- 6 files changed, 32 insertions(+), 37 deletions(-) delete mode 100644 .changeset/rename-sdk-shared-to-core.md diff --git a/.changeset/rename-sdk-shared-to-core.md b/.changeset/rename-sdk-shared-to-core.md deleted file mode 100644 index ba57d09b06..0000000000 --- a/.changeset/rename-sdk-shared-to-core.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@modelcontextprotocol/core': minor ---- - -The public Zod-schema package previously published (in alpha) as `@modelcontextprotocol/sdk-shared` is now `@modelcontextprotocol/core`. Update imports: `@modelcontextprotocol/sdk-shared` → `@modelcontextprotocol/core`. The v1→v2 codemod emits the new name automatically. (The private internal barrel formerly named `@modelcontextprotocol/core` is now `@modelcontextprotocol/core-internal` and is not published.) diff --git a/packages/codemod/test/v1-to-v2/authSchemaNames.test.ts b/packages/codemod/test/v1-to-v2/authSchemaNames.test.ts index 6269c15573..2fd4631c2d 100644 --- a/packages/codemod/test/v1-to-v2/authSchemaNames.test.ts +++ b/packages/codemod/test/v1-to-v2/authSchemaNames.test.ts @@ -12,15 +12,15 @@ describe('AUTH_SCHEMA_NAMES (codemod auth schema-routing allowlist)', () => { // the rewritten import would have no exported member. AUTH_SCHEMA_NAMES is the v1 auth-schema set, // a SUBSET of core's auth exports: core may export more (v2-only schemas such as // IdJagTokenExchangeResponseSchema) that v1 never had and the codemod never encounters. Read - // core's barrel directly (the `export { … } from '…/core/auth'` block) so they cannot drift. + // core's barrel directly (the `export { … } from '…/core-internal/auth'` block) so they cannot drift. const src = readFileSync(fileURLToPath(new URL('../../../core/src/index.ts', import.meta.url)), 'utf8'); const closeIdx = src.indexOf("} from '@modelcontextprotocol/core-internal/auth'"); const openIdx = src.lastIndexOf('export {', closeIdx); const block = src.slice(openIdx + 'export {'.length, closeIdx); - const sdkSharedAuthExports = new Set([...block.matchAll(/\b(\w+Schema)\b/g)].map(m => m[1])); + const coreAuthExports = new Set([...block.matchAll(/\b(\w+Schema)\b/g)].map(m => m[1])); - const notExportedBySdkShared = [...AUTH_SCHEMA_NAMES].filter(name => !sdkSharedAuthExports.has(name)); - expect(notExportedBySdkShared).toEqual([]); + const notExportedByCore = [...AUTH_SCHEMA_NAMES].filter(name => !coreAuthExports.has(name)); + expect(notExportedByCore).toEqual([]); // The v1 auth-schema set is frozen; pin its size so an accidental add/remove is caught. expect(AUTH_SCHEMA_NAMES.size).toBe(11); }); diff --git a/packages/codemod/test/v1-to-v2/specSchemaNames.test.ts b/packages/codemod/test/v1-to-v2/specSchemaNames.test.ts index d7f6bf3dd8..2a5aca1267 100644 --- a/packages/codemod/test/v1-to-v2/specSchemaNames.test.ts +++ b/packages/codemod/test/v1-to-v2/specSchemaNames.test.ts @@ -14,8 +14,8 @@ describe('SPEC_SCHEMA_NAMES (codemod schema-routing allowlist)', () => { // Read core's barrel directly so the two cannot silently drift. const src = readFileSync(fileURLToPath(new URL('../../../core/src/index.ts', import.meta.url)), 'utf8'); const block = src.slice(src.indexOf('export {') + 'export {'.length, src.indexOf('} from')); - const sdkSharedExports = [...new Set([...block.matchAll(/\b(\w+Schema)\b/g)].map(m => m[1]))].sort(); - expect([...SPEC_SCHEMA_NAMES].sort()).toEqual(sdkSharedExports); - expect(sdkSharedExports.length).toBeGreaterThanOrEqual(154); + const coreExports = [...new Set([...block.matchAll(/\b(\w+Schema)\b/g)].map(m => m[1]))].sort(); + expect([...SPEC_SCHEMA_NAMES].sort()).toEqual(coreExports); + expect(coreExports.length).toBeGreaterThanOrEqual(154); }); }); diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 2bfd2be28a..8ef16b453f 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -3,21 +3,21 @@ // Canonical public home for the Model Context Protocol specification + OAuth/OpenID Zod schemas. // // These are the exact schema constants the SDK validates against internally (defined in the -// private @modelcontextprotocol/core-internal package). This package bundles core and re-exports ONLY the +// private @modelcontextprotocol/core-internal package). This package bundles core-internal and re-exports ONLY the // `*Schema` Zod values, so consumers can validate protocol/OAuth payloads directly — e.g. -// `CallToolResultSchema.parse(value)` / `.safeParse(value)` — without depending on core's -// internal barrel. +// `CallToolResultSchema.parse(value)` / `.safeParse(value)` — without depending on core-internal's +// barrel. // // Scope: Zod schemas ONLY. The corresponding spec TypeScript types, error classes, enums, and // type guards are part of the public API of @modelcontextprotocol/server and /client. // -// Two groups, kept separate to mirror core's own spec-vs-auth split, each bundled from a build-only -// subpath alias of core (tsconfig.json + tsdown.config.ts): -// - SPEC schemas, from @modelcontextprotocol/core-internal/schemas (core/src/types/schemas.ts): every +// Two groups, kept separate to mirror core-internal's own spec-vs-auth split, each bundled from a build-only +// subpath alias of core-internal (tsconfig.json + tsdown.config.ts): +// - SPEC schemas, from @modelcontextprotocol/core-internal/schemas (core-internal/src/types/schemas.ts): every // `export const *Schema` EXCEPT internal helpers with no public spec type (e.g. -// BaseRequestParamsSchema). Mirrors core's SPEC_SCHEMA_KEYS allowlist. -// - OAUTH/OPENID schemas, from @modelcontextprotocol/core-internal/auth (core/src/shared/auth.ts). -// The sdkSharedSchemas test asserts both groups stay in sync with their core source modules. +// BaseRequestParamsSchema). Mirrors core-internal's SPEC_SCHEMA_KEYS allowlist. +// - OAUTH/OPENID schemas, from @modelcontextprotocol/core-internal/auth (core-internal/src/shared/auth.ts). +// The coreSchemas test asserts both groups stay in sync with their core-internal source modules. export { AnnotationsSchema, AudioContentSchema, @@ -176,12 +176,12 @@ export { } from '@modelcontextprotocol/core-internal/schemas'; // Auth schemas (OAuth / OpenID / IdJag) — kept as a SEPARATE group from the MCP spec schemas above, -// mirroring core's own spec-vs-auth split (these live in core/src/shared/auth.ts, not types/schemas.ts, -// and are registered as `authSchemas` in core's specTypeSchema.ts). This group is EXACTLY core's +// mirroring core-internal's own spec-vs-auth split (these live in core-internal/src/shared/auth.ts, not types/schemas.ts, +// and are registered as `authSchemas` in core-internal's specTypeSchema.ts). This group is EXACTLY core-internal's // `authSchemas` set — every auth schema that has a public spec type (so `isSpecType.OAuthTokens`, // `isSpecType.IdJagTokenExchangeResponse`, etc. exist). The typeless internal URL field-validators -// (SafeUrlSchema, OptionalSafeUrlSchema) are not auth schemas and stay out. The sdkSharedSchemas test -// asserts this group stays in sync with core's `authSchemas`. +// (SafeUrlSchema, OptionalSafeUrlSchema) are not auth schemas and stay out. The coreSchemas test +// asserts this group stays in sync with core-internal's `authSchemas`. export { IdJagTokenExchangeResponseSchema, OAuthClientInformationFullSchema, diff --git a/packages/core/test/coreSchemas.test.ts b/packages/core/test/coreSchemas.test.ts index 3571989c3b..85a0e08a09 100644 --- a/packages/core/test/coreSchemas.test.ts +++ b/packages/core/test/coreSchemas.test.ts @@ -3,7 +3,7 @@ import { fileURLToPath } from 'node:url'; import { describe, expect, it } from 'vitest'; -import * as sdkShared from '../src/index'; +import * as core from '../src/index'; import { CursorSchema, InitializeRequestSchema, OAuthTokensSchema } from '../src/index'; function readCore(relativePath: string): string { @@ -25,15 +25,15 @@ describe('@modelcontextprotocol/core', () => { }); it('re-exports exactly core’s spec + OAuth schemas — no internal helpers (drift guard)', () => { - // core's public surface is two SEPARATE groups, mirroring core's own spec-vs-auth split: - // 1. spec `*Schema` constants from core/src/types/schemas.ts (minus internal helpers with no - // public spec type — they must NOT leak), mirroring core's SPEC_SCHEMA_KEYS allowlist; and - // 2. the auth `*Schema` constants registered in core's `authSchemas` object (specTypeSchema.ts) + // core's public surface is two SEPARATE groups, mirroring core-internal's own spec-vs-auth split: + // 1. spec `*Schema` constants from core-internal/src/types/schemas.ts (minus internal helpers with no + // public spec type — they must NOT leak), mirroring core-internal's SPEC_SCHEMA_KEYS allowlist; and + // 2. the auth `*Schema` constants registered in core-internal's `authSchemas` object (specTypeSchema.ts) // — i.e. the auth schemas that have a public spec type. Reading that object directly (not a - // name prefix) is the source of truth, so a new auth schema added to core is required here + // name prefix) is the source of truth, so a new auth schema added to core-internal is required here // automatically; typeless internal helpers (SafeUrlSchema, OptionalSafeUrlSchema) stay out // because they are not in `authSchemas`. - // Read the core sources directly so the groups cannot silently drift. + // Read the core-internal sources directly so the groups cannot silently drift. const SPEC_INTERNAL_HELPERS = [ 'BaseRequestParamsSchema', 'ClientTasksCapabilitySchema', @@ -51,7 +51,7 @@ describe('@modelcontextprotocol/core', () => { const authSchemas = exportedSchemaConsts(authObj, /\b(\w+Schema)\b/g); const expected = [...specSchemas, ...authSchemas].sort(); - const exported = Object.keys(sdkShared).sort(); + const exported = Object.keys(core).sort(); // Exact match, both directions: a new core spec/auth schema missing here fails (we forgot to // re-export it), and any internal helper / non-spec symbol that leaks here also fails. expect(exported).toEqual(expected); diff --git a/packages/core/tsdown.config.ts b/packages/core/tsdown.config.ts index a0e3e12e76..464ae98615 100644 --- a/packages/core/tsdown.config.ts +++ b/packages/core/tsdown.config.ts @@ -1,11 +1,11 @@ import { defineConfig } from 'tsdown'; // core re-exports ONLY the spec + OAuth Zod schemas from @modelcontextprotocol/core-internal (private, -// unpublished). Two BUILD-ONLY subpath aliases (not real core exports) point at core's two schema +// unpublished). Two BUILD-ONLY subpath aliases (not real core exports) point at core-internal's two schema // modules, kept as separate sources: -// @modelcontextprotocol/core-internal/schemas → core/src/types/schemas.ts (MCP spec schemas) -// @modelcontextprotocol/core-internal/auth → core/src/shared/auth.ts (OAuth/OpenID schemas) -// Aliasing to these modules rather than core's barrel keeps the bundled graph to just the schemas + +// @modelcontextprotocol/core-internal/schemas → core-internal/src/types/schemas.ts (MCP spec schemas) +// @modelcontextprotocol/core-internal/auth → core-internal/src/shared/auth.ts (OAuth/OpenID schemas) +// Aliasing to these modules rather than core-internal's barrel keeps the bundled graph to just the schemas + // the constants they use — never Protocol, transports, stdio, or the ajv/cfWorker validators. Both // modules import only `zod/v4`, so the graph stays runtime-neutral; `platform: 'neutral'` makes a // node-only dependency leaking in fail the build here instead of silently shipping. From 68b2120a3b90e61249cc42baa900c8c45decadbf Mon Sep 17 00:00:00 2001 From: Konstantin Konstantinov Date: Thu, 25 Jun 2026 20:19:10 +0300 Subject: [PATCH 21/21] fix(codemod): route destructured .then() dynamic-import param; fix stale validator comment MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - mockPaths: import('…/types.js').then(({ CallToolResultSchema }) => …) was silently rewritten to the context package (server/client, no Zod schema exports) with no diagnostic. Generalize destructure handling (getModuleBindingPattern) so a .then(({…}) => …) param is routed/renamed per-symbol like 'const { … } = await import()' — schema-only → core, mixed flagged. +3 tests. - core-internal index.ts: validator-import comment listed @modelcontextprotocol/core/validators/*, which doesn't exist after the rename (core exports only '.'); point at {client,server}. --- .../v1-to-v2/transforms/mockPaths.ts | 95 +++++++++++++------ .../v1-to-v2/transforms/mockPaths.test.ts | 31 ++++++ packages/core-internal/src/index.ts | 2 +- 3 files changed, 96 insertions(+), 32 deletions(-) diff --git a/packages/codemod/src/migrations/v1-to-v2/transforms/mockPaths.ts b/packages/codemod/src/migrations/v1-to-v2/transforms/mockPaths.ts index c370ba4fce..7ffdd53887 100644 --- a/packages/codemod/src/migrations/v1-to-v2/transforms/mockPaths.ts +++ b/packages/codemod/src/migrations/v1-to-v2/transforms/mockPaths.ts @@ -252,19 +252,45 @@ function renameSymbolsInFactory(factoryArg: import('ts-morph').Node, renamedSymb } /** - * The destructured binding keys of an `await import()` assigned to an object binding pattern (e.g. - * `const { CallToolResultSchema, McpError } = await import('…')`), or `[]` for a non-destructured - * binding, a `.then()` chain, or an unassigned `await import()`. The keys feed per-symbol routing — - * the specifier itself can't be split. + * The object binding pattern through which a dynamic import's named symbols are pulled — either + * `const { … } = await import('…')` or `import('…').then(({ … }) => …)`. Returns undefined for a + * non-destructured binding (`const mod = await import()`), an identifier `.then` param (`m => …`), or + * an unassigned `await import()`. Both destructured shapes expose named symbols that can be routed and + * renamed per-symbol (the specifier itself can't be split). */ -function getDestructuredKeys(node: import('ts-morph').CallExpression): string[] { +function getModuleBindingPattern(node: import('ts-morph').CallExpression): import('ts-morph').ObjectBindingPattern | undefined { const parent = node.getParent(); - if (!parent || !Node.isAwaitExpression(parent)) return []; - const grandParent = parent.getParent(); - if (!grandParent || !Node.isVariableDeclaration(grandParent)) return []; - const nameNode = grandParent.getNameNode(); - if (!Node.isObjectBindingPattern(nameNode)) return []; - return nameNode.getElements().map(el => el.getPropertyNameNode()?.getText() ?? el.getName()); + if (parent && Node.isAwaitExpression(parent)) { + const grandParent = parent.getParent(); + if (grandParent && Node.isVariableDeclaration(grandParent)) { + const nameNode = grandParent.getNameNode(); + if (Node.isObjectBindingPattern(nameNode)) return nameNode; + } + return undefined; + } + if (parent && Node.isPropertyAccessExpression(parent) && parent.getName() === 'then') { + const thenCall = parent.getParent(); + if (thenCall && Node.isCallExpression(thenCall)) { + const cb = thenCall.getArguments()[0]; + if (cb && (Node.isArrowFunction(cb) || Node.isFunctionExpression(cb))) { + const paramName = cb.getParameters()[0]?.getNameNode(); + if (paramName && Node.isObjectBindingPattern(paramName)) return paramName; + } + } + } + return undefined; +} + +/** + * The destructured binding keys of a dynamic import — for both `const { … } = await import('…')` and + * `import('…').then(({ … }) => …)` — or `[]` for a non-destructured binding, an identifier `.then` + * param, or an unassigned `await import()`. The keys feed per-symbol routing; the specifier itself + * can't be split. + */ +function getDestructuredKeys(node: import('ts-morph').CallExpression): string[] { + const pattern = getModuleBindingPattern(node); + if (!pattern) return []; + return pattern.getElements().map(el => el.getPropertyNameNode()?.getText() ?? el.getName()); } /** @@ -418,29 +444,36 @@ function rewriteDynamicImports( firstArg.setLiteralValue(effectiveTarget); changes++; - const parent = node.getParent(); - if (parent && Node.isAwaitExpression(parent)) { - const grandParent = parent.getParent(); - if (grandParent && Node.isVariableDeclaration(grandParent)) { - const nameNode = grandParent.getNameNode(); - if (Node.isObjectBindingPattern(nameNode)) { - for (const element of nameNode.getElements()) { - const propertyName = element.getPropertyNameNode()?.getText(); - const bindingName = element.getName(); - const lookupKey = propertyName ?? bindingName; - const newName = allRenames[lookupKey]; - if (newName) { - if (propertyName) { - element.getPropertyNameNode()!.replaceWithText(newName); - } else { - element.replaceWithText(`${newName}: ${bindingName}`); - } - changes++; - } + // Apply symbol renames to the destructured binding elements — for both `await import()` + // destructuring and a `.then(({ … }) => …)` param (both routed per-symbol above when their + // symbols share a target, e.g. schema-only → core). + const bindingPattern = getModuleBindingPattern(node); + if (bindingPattern) { + for (const element of bindingPattern.getElements()) { + const propertyName = element.getPropertyNameNode()?.getText(); + const bindingName = element.getName(); + const lookupKey = propertyName ?? bindingName; + const newName = allRenames[lookupKey]; + if (newName) { + if (propertyName) { + element.getPropertyNameNode()!.replaceWithText(newName); + } else { + element.replaceWithText(`${newName}: ${bindingName}`); } + changes++; } + } + } + + // A non-destructured awaited binding (`const mod = await import('…')`) can't have per-symbol + // renames applied, so flag them if the mapping carries any. (Identifier `.then` params and bare + // `mod.Schema` accesses are surfaced by `collectModuleSchemaAccesses` above.) + const awaitParent = node.getParent(); + if (awaitParent && Node.isAwaitExpression(awaitParent)) { + const decl = awaitParent.getParent(); + if (decl && Node.isVariableDeclaration(decl) && !Node.isObjectBindingPattern(decl.getNameNode())) { const moduleRenames = resolved.mapping.renamedSymbols ?? {}; - if (!Node.isObjectBindingPattern(nameNode) && Object.keys(moduleRenames).length > 0) { + if (Object.keys(moduleRenames).length > 0) { diagnostics.push( actionRequired( sourceFile.getFilePath(), diff --git a/packages/codemod/test/v1-to-v2/transforms/mockPaths.test.ts b/packages/codemod/test/v1-to-v2/transforms/mockPaths.test.ts index f00f921b99..ed613ddc2f 100644 --- a/packages/codemod/test/v1-to-v2/transforms/mockPaths.test.ts +++ b/packages/codemod/test/v1-to-v2/transforms/mockPaths.test.ts @@ -400,6 +400,37 @@ describe('mock-paths transform', () => { const result = mockPathsTransform.apply(sourceFile, ctx); expect(result.diagnostics.some(d => d.message.includes('belong to different v2 packages'))).toBe(true); }); + + it('routes a destructured .then() param of only *Schema constants to core', () => { + const input = [ + `import('@modelcontextprotocol/sdk/types.js').then(({ CallToolResultSchema }) => CallToolResultSchema.parse(value));`, + '' + ].join('\n'); + const result = applyTransform(input); + expect(result).toContain(`import('@modelcontextprotocol/core')`); + expect(result).not.toContain('@modelcontextprotocol/sdk/types'); + }); + + it('renames a *Schema in a destructured .then() param and routes it to core', () => { + const input = [ + `import('@modelcontextprotocol/sdk/types.js').then(({ JSONRPCResponseSchema }) => JSONRPCResponseSchema.parse(value));`, + '' + ].join('\n'); + const result = applyTransform(input); + expect(result).toContain(`import('@modelcontextprotocol/core')`); + expect(result).toContain('JSONRPCResultResponseSchema'); + }); + + it('flags a destructured .then() param mixing a *Schema constant and a type', () => { + const input = [ + `import('@modelcontextprotocol/sdk/types.js').then(({ CallToolResultSchema, McpError }) => CallToolResultSchema.parse(value));`, + '' + ].join('\n'); + const project = new Project({ useInMemoryFileSystem: true }); + const sourceFile = project.createSourceFile('test.ts', input); + const result = mockPathsTransform.apply(sourceFile, ctx); + expect(result.diagnostics.some(d => d.message.includes('belong to different v2 packages'))).toBe(true); + }); }); describe('lazy context resolution (no spurious project-type diagnostic)', () => { diff --git a/packages/core-internal/src/index.ts b/packages/core-internal/src/index.ts index 940ab08187..fb89b0383d 100644 --- a/packages/core-internal/src/index.ts +++ b/packages/core-internal/src/index.ts @@ -15,7 +15,7 @@ export * from './util/standardSchema'; export * from './util/zodCompat'; // Validator providers are type-only here — import the runtime classes from the explicit -// `@modelcontextprotocol/{core,client,server}/validators/{ajv,cf-worker}` subpaths to customise. +// `@modelcontextprotocol/{client,server}/validators/{ajv,cf-worker}` subpaths to customise. export type { AjvJsonSchemaValidator } from './validators/ajvProvider'; export type { CfWorkerJsonSchemaValidator, CfWorkerSchemaDraft } from './validators/cfWorkerProvider'; export * from './validators/fromJsonSchema';