diff --git a/packages/codemod/src/bin/batchTest.ts b/packages/codemod/src/bin/batchTest.ts index 805d315585..0e45e1715b 100644 --- a/packages/codemod/src/bin/batchTest.ts +++ b/packages/codemod/src/bin/batchTest.ts @@ -87,6 +87,7 @@ const LOCAL_PACKAGE_DIRS: Record = { '@modelcontextprotocol/client': path.join(SDK_ROOT, 'packages/client'), '@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/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/migrations/v1-to-v2/mappings/importMap.ts b/packages/codemod/src/migrations/v1-to-v2/mappings/importMap.ts index ad689cdc46..24d086f8de 100644 --- a/packages/codemod/src/migrations/v1-to-v2/mappings/importMap.ts +++ b/packages/codemod/src/migrations/v1-to-v2/mappings/importMap.ts @@ -62,7 +62,11 @@ export const IMPORT_MAP: Record = { StreamableHTTPServerTransport: 'NodeStreamableHTTPServerTransport' }, symbolTargetOverrides: { - StreamableHTTPServerTransport: '@modelcontextprotocol/node' + StreamableHTTPServerTransport: '@modelcontextprotocol/node', + // The companion options type moved with the transport. @modelcontextprotocol/node + // re-exports it under the same name (a backward-compat alias for + // WebStandardStreamableHTTPServerTransportOptions), so route it there without renaming. + StreamableHTTPServerTransportOptions: '@modelcontextprotocol/node' } }, '@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js': { diff --git a/packages/codemod/src/migrations/v1-to-v2/transforms/specSchemaAccess.ts b/packages/codemod/src/migrations/v1-to-v2/transforms/specSchemaAccess.ts index 015c706abc..79f4a0a707 100644 --- a/packages/codemod/src/migrations/v1-to-v2/transforms/specSchemaAccess.ts +++ b/packages/codemod/src/migrations/v1-to-v2/transforms/specSchemaAccess.ts @@ -101,28 +101,15 @@ function handleReference( return rewriteCapturedSafeParse(safeParseCall, localName, typeName, sourceFile, diagnostics); } - diagnostics.push( - actionRequired( - sourceFile.getFilePath(), - ref, - `${localName}.safeParse() not available in v2. Use \`isSpecType.${typeName}(value)\` for boolean validation, ` + - `or \`specTypeSchemas.${typeName}['~standard'].validate(value)\` for full result.` - ) - ); - return false; + return rewriteUnsupportedSchemaCall(ref, safeParseCall, localName, typeName, 'safeParse', sourceFile, diagnostics); } - // Pattern: XSchema.parse(v) — diagnostic only + // 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)) { - diagnostics.push( - actionRequired( - sourceFile.getFilePath(), - ref, - `${localName}.parse() not available in v2. Use \`isSpecType.${typeName}(value)\` for validation, ` + - `or \`specTypeSchemas.${typeName}['~standard'].validate(value)\` and check for issues.` - ) - ); - return false; + 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.) @@ -327,6 +314,68 @@ function rewriteCapturedSafeParse( 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; diff --git a/packages/codemod/test/v1-to-v2/transforms/specSchemaAccess.test.ts b/packages/codemod/test/v1-to-v2/transforms/specSchemaAccess.test.ts index 6909083e79..2c7f592e1f 100644 --- a/packages/codemod/test/v1-to-v2/transforms/specSchemaAccess.test.ts +++ b/packages/codemod/test/v1-to-v2/transforms/specSchemaAccess.test.ts @@ -210,10 +210,12 @@ describe('spec-schema-access transform', () => { expect(text).not.toContain('return result.value'); }); - it('falls back to diagnostic for non-captured safeParse (bare expression)', () => { + it('rewrites non-captured safeParse (bare expression) to validate()', () => { const input = [`import { ToolSchema } from '@modelcontextprotocol/server';`, `ToolSchema.safeParse(data);`, ''].join('\n'); - const { result } = applyTransform(input); - expect(result.changesCount).toBe(0); + 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); }); }); @@ -327,16 +329,24 @@ describe('spec-schema-access transform', () => { }); }); - describe('diagnostic only: .parse(v)', () => { - it('emits diagnostic for parse usage', () => { + 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('ToolSchema.parse'); - expect(result.changesCount).toBe(0); - expect(result.diagnostics.length).toBe(1); - expect(result.diagnostics[0]!.message).toContain('isSpecType.Tool'); + 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'); }); }); @@ -392,7 +402,7 @@ describe('spec-schema-access transform', () => { expect(text).not.toMatch(/import\s*\{[^}]*CallToolRequestSchema[^}]*\}/); }); - it('keeps original schema import when some refs are diagnostic-only', () => { + 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;`, @@ -401,8 +411,8 @@ describe('spec-schema-access transform', () => { ].join('\n'); const { text } = applyTransform(input); expect(text).toContain('isSpecType.CallToolRequest(data)'); - expect(text).toContain('CallToolRequestSchema.parse'); - expect(text).toMatch(/import\s*\{[^}]*CallToolRequestSchema[^}]*\}/); + 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', () => {