Skip to content

Commit 99cf12f

Browse files
committed
feat(codemod): rewrite v1 validator paths to the v2 subpaths
v1 source code that imports built-in validators looks like: import { CfWorkerJsonSchemaValidator } from '@modelcontextprotocol/sdk/validation/cfworker-provider.js'; The v1-to-v2 codemod now rewrites this to the v2 subpath that ships the named class, picking the right base package per file: import { CfWorkerJsonSchemaValidator } from '@modelcontextprotocol/{client,server}/validators/cf-worker'; Covers all six v1 paths users wrote in the wild: validation/ajv-provider.js, validation/ajv.js, validation/ajv validation/cfworker-provider.js, validation/cfworker.js, validation/cfworker plus the (rarer) validation/index.js and validation/types.js barrels which carry only the jsonSchemaValidator interface — those route to the base client/server package, which already re-exports those types. Resolution uses the same sibling-import + project-type heuristic as the rest of the importPaths transform: if the file already imports from sdk/client/* it routes to /client/validators/..., otherwise it routes to /server/validators/..., otherwise it falls back to the project type from package.json. Adds a subpathSuffix field to ImportMapping so the existing RESOLVE_BY_CONTEXT logic can append a subpath after picking the base. Eight new tests cover all six v1 paths, both base packages, and the usedPackages output. Verified against the agents repo (the real-world v1 caller this PR is unblocking).
1 parent f94ceb6 commit 99cf12f

5 files changed

Lines changed: 163 additions & 0 deletions

File tree

packages/codemod/src/migrations/v1-to-v2/mappings/importMap.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,11 @@ export interface ImportMapping {
77
removalMessage?: string;
88
/** No entries currently set this; scaffolding for when a v1 symbol has no v2 equivalent yet. */
99
isV2Gap?: boolean;
10+
/**
11+
* Subpath suffix appended after `RESOLVE_BY_CONTEXT` resolves the base package (e.g. `/validators/ajv`).
12+
* The final target becomes `@modelcontextprotocol/{client,server}<subpathSuffix>`.
13+
*/
14+
subpathSuffix?: string;
1015
}
1116

1217
export const IMPORT_MAP: Record<string, ImportMapping> = {
@@ -163,6 +168,27 @@ export const IMPORT_MAP: Record<string, ImportMapping> = {
163168
}
164169
};
165170

171+
// v1 `validation/*` paths → v2 `validators/*` subpaths. The canonical `*-provider.js` filename and
172+
// the short aliases from the v1 README, with and without `.js` suffix.
173+
const VALIDATOR_V1_VARIANTS: Record<string, readonly string[]> = {
174+
'/validators/ajv': ['validation/ajv-provider.js', 'validation/ajv.js', 'validation/ajv'],
175+
'/validators/cf-worker': ['validation/cfworker-provider.js', 'validation/cfworker.js', 'validation/cfworker']
176+
};
177+
for (const [subpathSuffix, v1Specifiers] of Object.entries(VALIDATOR_V1_VARIANTS)) {
178+
for (const v1Specifier of v1Specifiers) {
179+
IMPORT_MAP[`@modelcontextprotocol/sdk/${v1Specifier}`] = {
180+
target: 'RESOLVE_BY_CONTEXT',
181+
status: 'moved',
182+
subpathSuffix
183+
};
184+
}
185+
}
186+
187+
// `validation/index` / `validation/types` carry only the `jsonSchemaValidator` interface + helpers.
188+
for (const barrelSpecifier of ['@modelcontextprotocol/sdk/validation/index.js', '@modelcontextprotocol/sdk/validation/types.js']) {
189+
IMPORT_MAP[barrelSpecifier] = { target: 'RESOLVE_BY_CONTEXT', status: 'moved' };
190+
}
191+
166192
export function isAuthImport(specifier: string): boolean {
167193
return specifier.includes('/server/auth/') || specifier.includes('/server/auth.');
168194
}

packages/codemod/src/migrations/v1-to-v2/transforms/importPaths.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,9 @@ export const importPathsTransform: Transform = {
102102
line,
103103
diagnostics
104104
});
105+
if (mapping.subpathSuffix) {
106+
targetPackage = `${targetPackage}${mapping.subpathSuffix}`;
107+
}
105108
}
106109

107110
const symbolsToRenameInFile: Array<[string, string]> = [];
@@ -250,6 +253,9 @@ function rewriteExportDeclarations(
250253
return spec.includes('/server/') || spec === '@modelcontextprotocol/server';
251254
});
252255
targetPackage = resolveTypesPackage(context, hasClientImport, hasServerImport);
256+
if (mapping.subpathSuffix) {
257+
targetPackage = `${targetPackage}${mapping.subpathSuffix}`;
258+
}
253259
}
254260

255261
if (mapping.symbolTargetOverrides) {

packages/codemod/src/migrations/v1-to-v2/transforms/mockPaths.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,9 @@ function resolveTarget(
6464
return s.includes('/server/') || s === '@modelcontextprotocol/server';
6565
});
6666
target = resolveTypesPackage(context, hasClient, hasServer, diagnosticSink);
67+
if (mapping.subpathSuffix) {
68+
target = `${target}${mapping.subpathSuffix}`;
69+
}
6770
}
6871

6972
return { target, renamedSymbols: mapping.renamedSymbols, symbolTargetOverrides: mapping.symbolTargetOverrides };

packages/codemod/test/v1-to-v2/transforms/importPaths.test.ts

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -448,4 +448,85 @@ describe('import-paths transform', () => {
448448
expect(result).toContain(`from "@modelcontextprotocol/client"`);
449449
expect(result).toContain('InMemoryTransport');
450450
});
451+
452+
describe('validator subpath rewrites', () => {
453+
it('rewrites CfWorkerJsonSchemaValidator from v1 cfworker-provider to client subpath', () => {
454+
const input = [
455+
`import { Client } from '@modelcontextprotocol/sdk/client/index.js';`,
456+
`import { CfWorkerJsonSchemaValidator } from '@modelcontextprotocol/sdk/validation/cfworker-provider.js';`,
457+
''
458+
].join('\n');
459+
const result = applyTransform(input, { projectType: 'client' });
460+
expect(result).toContain(`from "@modelcontextprotocol/client/validators/cf-worker"`);
461+
expect(result).toContain('CfWorkerJsonSchemaValidator');
462+
expect(result).not.toContain('@modelcontextprotocol/sdk');
463+
});
464+
465+
it('rewrites CfWorkerJsonSchemaValidator from v1 cfworker short alias to server subpath', () => {
466+
const input = [
467+
`import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';`,
468+
`import { CfWorkerJsonSchemaValidator } from '@modelcontextprotocol/sdk/validation/cfworker';`,
469+
''
470+
].join('\n');
471+
const result = applyTransform(input, { projectType: 'server' });
472+
expect(result).toContain(`from "@modelcontextprotocol/server/validators/cf-worker"`);
473+
expect(result).toContain('CfWorkerJsonSchemaValidator');
474+
});
475+
476+
it('rewrites AjvJsonSchemaValidator from v1 ajv-provider to server subpath', () => {
477+
const input = [
478+
`import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';`,
479+
`import { AjvJsonSchemaValidator } from '@modelcontextprotocol/sdk/validation/ajv-provider.js';`,
480+
''
481+
].join('\n');
482+
const result = applyTransform(input, { projectType: 'server' });
483+
expect(result).toContain(`from "@modelcontextprotocol/server/validators/ajv"`);
484+
expect(result).toContain('AjvJsonSchemaValidator');
485+
});
486+
487+
it('rewrites AjvJsonSchemaValidator from v1 ajv short alias to server subpath', () => {
488+
const input = [
489+
`import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';`,
490+
`import { AjvJsonSchemaValidator } from '@modelcontextprotocol/sdk/validation/ajv';`,
491+
''
492+
].join('\n');
493+
const result = applyTransform(input, { projectType: 'server' });
494+
expect(result).toContain(`from "@modelcontextprotocol/server/validators/ajv"`);
495+
expect(result).toContain('AjvJsonSchemaValidator');
496+
});
497+
498+
it('routes validator subpath to client when only client siblings exist', () => {
499+
const input = [
500+
`import { Client } from '@modelcontextprotocol/sdk/client/index.js';`,
501+
`import { AjvJsonSchemaValidator } from '@modelcontextprotocol/sdk/validation/ajv-provider.js';`,
502+
''
503+
].join('\n');
504+
const result = applyTransform(input, { projectType: 'both' });
505+
expect(result).toContain(`from "@modelcontextprotocol/client/validators/ajv"`);
506+
});
507+
508+
it('routes validator subpath via project type when no SDK siblings', () => {
509+
const input = `import { CfWorkerJsonSchemaValidator } from '@modelcontextprotocol/sdk/validation/cfworker-provider.js';\n`;
510+
const result = applyTransform(input, { projectType: 'server' });
511+
expect(result).toContain(`from "@modelcontextprotocol/server/validators/cf-worker"`);
512+
});
513+
514+
it('includes the validator subpath in usedPackages', () => {
515+
const project = new Project({ useInMemoryFileSystem: true });
516+
const sourceFile = project.createSourceFile(
517+
'test.ts',
518+
`import { CfWorkerJsonSchemaValidator } from '@modelcontextprotocol/sdk/validation/cfworker-provider.js';\n`
519+
);
520+
const result = importPathsTransform.apply(sourceFile, { projectType: 'client' });
521+
expect(result.usedPackages).toBeDefined();
522+
expect(result.usedPackages!.has('@modelcontextprotocol/client/validators/cf-worker')).toBe(true);
523+
});
524+
525+
it('rewrites the validation/index type-only import to the resolved base package', () => {
526+
const input = `import type { jsonSchemaValidator } from '@modelcontextprotocol/sdk/validation/index.js';\n`;
527+
const result = applyTransform(input, { projectType: 'server' });
528+
expect(result).toContain(`from "@modelcontextprotocol/server"`);
529+
expect(result).toContain('jsonSchemaValidator');
530+
});
531+
});
451532
});

packages/codemod/test/v1-to-v2/transforms/mockPaths.test.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -315,4 +315,51 @@ describe('mock-paths transform', () => {
315315
expect(result).toContain('new MyError(');
316316
});
317317
});
318+
319+
describe('validator subpath rewrites', () => {
320+
it('rewrites vi.mock of validator provider to the subpath', () => {
321+
const input = [
322+
`vi.mock('@modelcontextprotocol/sdk/validation/cfworker-provider.js', () => ({`,
323+
` CfWorkerJsonSchemaValidator: vi.fn()`,
324+
`}));`,
325+
''
326+
].join('\n');
327+
const result = applyTransform(input);
328+
expect(result).toContain(`'@modelcontextprotocol/server/validators/cf-worker'`);
329+
expect(result).not.toContain('@modelcontextprotocol/sdk');
330+
});
331+
332+
it('rewrites vi.doMock of ajv provider with sibling client import', () => {
333+
const input = [
334+
`import { Client } from '@modelcontextprotocol/sdk/client/index.js';`,
335+
`vi.doMock('@modelcontextprotocol/sdk/validation/ajv-provider.js', () => ({`,
336+
` AjvJsonSchemaValidator: vi.fn()`,
337+
`}));`,
338+
''
339+
].join('\n');
340+
const result = applyTransform(input, { projectType: 'both' });
341+
expect(result).toContain(`'@modelcontextprotocol/client/validators/ajv'`);
342+
});
343+
344+
it('rewrites dynamic import of validator provider to the subpath', () => {
345+
const input = [
346+
`const { AjvJsonSchemaValidator } = await import('@modelcontextprotocol/sdk/validation/ajv-provider.js');`,
347+
''
348+
].join('\n');
349+
const result = applyTransform(input);
350+
expect(result).toContain(`'@modelcontextprotocol/server/validators/ajv'`);
351+
expect(result).toContain('AjvJsonSchemaValidator');
352+
});
353+
354+
it('rewrites jest.mock of validator short alias to the subpath', () => {
355+
const input = [
356+
`jest.mock('@modelcontextprotocol/sdk/validation/cfworker', () => ({`,
357+
` CfWorkerJsonSchemaValidator: jest.fn()`,
358+
`}));`,
359+
''
360+
].join('\n');
361+
const result = applyTransform(input);
362+
expect(result).toContain(`'@modelcontextprotocol/server/validators/cf-worker'`);
363+
});
364+
});
318365
});

0 commit comments

Comments
 (0)