feat(client,server): bundle default validators, expose customisation via subpaths (#2088)
This commit is contained in:
parent
4a5c863a21
commit
16d13abf78
5 changed files with 749 additions and 314 deletions
|
|
@ -7,6 +7,11 @@ export interface ImportMapping {
|
|||
removalMessage?: string;
|
||||
/** No entries currently set this; scaffolding for when a v1 symbol has no v2 equivalent yet. */
|
||||
isV2Gap?: boolean;
|
||||
/**
|
||||
* Subpath suffix appended after `RESOLVE_BY_CONTEXT` resolves the base package (e.g. `/validators/ajv`).
|
||||
* The final target becomes `@modelcontextprotocol/{client,server}<subpathSuffix>`.
|
||||
*/
|
||||
subpathSuffix?: string;
|
||||
}
|
||||
|
||||
export const IMPORT_MAP: Record<string, ImportMapping> = {
|
||||
|
|
@ -163,6 +168,27 @@ export const IMPORT_MAP: Record<string, ImportMapping> = {
|
|||
}
|
||||
};
|
||||
|
||||
// v1 `validation/*` paths → v2 `validators/*` subpaths. The canonical `*-provider.js` filename and
|
||||
// the short aliases from the v1 README, with and without `.js` suffix.
|
||||
const VALIDATOR_V1_VARIANTS: Record<string, readonly string[]> = {
|
||||
'/validators/ajv': ['validation/ajv-provider.js', 'validation/ajv.js', 'validation/ajv'],
|
||||
'/validators/cf-worker': ['validation/cfworker-provider.js', 'validation/cfworker.js', 'validation/cfworker']
|
||||
};
|
||||
for (const [subpathSuffix, v1Specifiers] of Object.entries(VALIDATOR_V1_VARIANTS)) {
|
||||
for (const v1Specifier of v1Specifiers) {
|
||||
IMPORT_MAP[`@modelcontextprotocol/sdk/${v1Specifier}`] = {
|
||||
target: 'RESOLVE_BY_CONTEXT',
|
||||
status: 'moved',
|
||||
subpathSuffix
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// `validation/index` / `validation/types` carry only the `jsonSchemaValidator` interface + helpers.
|
||||
for (const barrelSpecifier of ['@modelcontextprotocol/sdk/validation/index.js', '@modelcontextprotocol/sdk/validation/types.js']) {
|
||||
IMPORT_MAP[barrelSpecifier] = { target: 'RESOLVE_BY_CONTEXT', status: 'moved' };
|
||||
}
|
||||
|
||||
export function isAuthImport(specifier: string): boolean {
|
||||
return specifier.includes('/server/auth/') || specifier.includes('/server/auth.');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -102,6 +102,9 @@ export const importPathsTransform: Transform = {
|
|||
line,
|
||||
diagnostics
|
||||
});
|
||||
if (mapping.subpathSuffix) {
|
||||
targetPackage = `${targetPackage}${mapping.subpathSuffix}`;
|
||||
}
|
||||
}
|
||||
|
||||
const symbolsToRenameInFile: Array<[string, string]> = [];
|
||||
|
|
@ -250,6 +253,9 @@ function rewriteExportDeclarations(
|
|||
return spec.includes('/server/') || spec === '@modelcontextprotocol/server';
|
||||
});
|
||||
targetPackage = resolveTypesPackage(context, hasClientImport, hasServerImport);
|
||||
if (mapping.subpathSuffix) {
|
||||
targetPackage = `${targetPackage}${mapping.subpathSuffix}`;
|
||||
}
|
||||
}
|
||||
|
||||
if (mapping.symbolTargetOverrides) {
|
||||
|
|
|
|||
|
|
@ -64,6 +64,9 @@ function resolveTarget(
|
|||
return s.includes('/server/') || s === '@modelcontextprotocol/server';
|
||||
});
|
||||
target = resolveTypesPackage(context, hasClient, hasServer, diagnosticSink);
|
||||
if (mapping.subpathSuffix) {
|
||||
target = `${target}${mapping.subpathSuffix}`;
|
||||
}
|
||||
}
|
||||
|
||||
return { target, renamedSymbols: mapping.renamedSymbols, symbolTargetOverrides: mapping.symbolTargetOverrides };
|
||||
|
|
|
|||
|
|
@ -448,4 +448,85 @@ describe('import-paths transform', () => {
|
|||
expect(result).toContain(`from "@modelcontextprotocol/client"`);
|
||||
expect(result).toContain('InMemoryTransport');
|
||||
});
|
||||
|
||||
describe('validator subpath rewrites', () => {
|
||||
it('rewrites CfWorkerJsonSchemaValidator from v1 cfworker-provider to client subpath', () => {
|
||||
const input = [
|
||||
`import { Client } from '@modelcontextprotocol/sdk/client/index.js';`,
|
||||
`import { CfWorkerJsonSchemaValidator } from '@modelcontextprotocol/sdk/validation/cfworker-provider.js';`,
|
||||
''
|
||||
].join('\n');
|
||||
const result = applyTransform(input, { projectType: 'client' });
|
||||
expect(result).toContain(`from "@modelcontextprotocol/client/validators/cf-worker"`);
|
||||
expect(result).toContain('CfWorkerJsonSchemaValidator');
|
||||
expect(result).not.toContain('@modelcontextprotocol/sdk');
|
||||
});
|
||||
|
||||
it('rewrites CfWorkerJsonSchemaValidator from v1 cfworker short alias to server subpath', () => {
|
||||
const input = [
|
||||
`import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';`,
|
||||
`import { CfWorkerJsonSchemaValidator } from '@modelcontextprotocol/sdk/validation/cfworker';`,
|
||||
''
|
||||
].join('\n');
|
||||
const result = applyTransform(input, { projectType: 'server' });
|
||||
expect(result).toContain(`from "@modelcontextprotocol/server/validators/cf-worker"`);
|
||||
expect(result).toContain('CfWorkerJsonSchemaValidator');
|
||||
});
|
||||
|
||||
it('rewrites AjvJsonSchemaValidator from v1 ajv-provider to server subpath', () => {
|
||||
const input = [
|
||||
`import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';`,
|
||||
`import { AjvJsonSchemaValidator } from '@modelcontextprotocol/sdk/validation/ajv-provider.js';`,
|
||||
''
|
||||
].join('\n');
|
||||
const result = applyTransform(input, { projectType: 'server' });
|
||||
expect(result).toContain(`from "@modelcontextprotocol/server/validators/ajv"`);
|
||||
expect(result).toContain('AjvJsonSchemaValidator');
|
||||
});
|
||||
|
||||
it('rewrites AjvJsonSchemaValidator from v1 ajv short alias to server subpath', () => {
|
||||
const input = [
|
||||
`import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';`,
|
||||
`import { AjvJsonSchemaValidator } from '@modelcontextprotocol/sdk/validation/ajv';`,
|
||||
''
|
||||
].join('\n');
|
||||
const result = applyTransform(input, { projectType: 'server' });
|
||||
expect(result).toContain(`from "@modelcontextprotocol/server/validators/ajv"`);
|
||||
expect(result).toContain('AjvJsonSchemaValidator');
|
||||
});
|
||||
|
||||
it('routes validator subpath to client when only client siblings exist', () => {
|
||||
const input = [
|
||||
`import { Client } from '@modelcontextprotocol/sdk/client/index.js';`,
|
||||
`import { AjvJsonSchemaValidator } from '@modelcontextprotocol/sdk/validation/ajv-provider.js';`,
|
||||
''
|
||||
].join('\n');
|
||||
const result = applyTransform(input, { projectType: 'both' });
|
||||
expect(result).toContain(`from "@modelcontextprotocol/client/validators/ajv"`);
|
||||
});
|
||||
|
||||
it('routes validator subpath via project type when no SDK siblings', () => {
|
||||
const input = `import { CfWorkerJsonSchemaValidator } from '@modelcontextprotocol/sdk/validation/cfworker-provider.js';\n`;
|
||||
const result = applyTransform(input, { projectType: 'server' });
|
||||
expect(result).toContain(`from "@modelcontextprotocol/server/validators/cf-worker"`);
|
||||
});
|
||||
|
||||
it('includes the validator subpath in usedPackages', () => {
|
||||
const project = new Project({ useInMemoryFileSystem: true });
|
||||
const sourceFile = project.createSourceFile(
|
||||
'test.ts',
|
||||
`import { CfWorkerJsonSchemaValidator } from '@modelcontextprotocol/sdk/validation/cfworker-provider.js';\n`
|
||||
);
|
||||
const result = importPathsTransform.apply(sourceFile, { projectType: 'client' });
|
||||
expect(result.usedPackages).toBeDefined();
|
||||
expect(result.usedPackages!.has('@modelcontextprotocol/client/validators/cf-worker')).toBe(true);
|
||||
});
|
||||
|
||||
it('rewrites the validation/index type-only import to the resolved base package', () => {
|
||||
const input = `import type { jsonSchemaValidator } from '@modelcontextprotocol/sdk/validation/index.js';\n`;
|
||||
const result = applyTransform(input, { projectType: 'server' });
|
||||
expect(result).toContain(`from "@modelcontextprotocol/server"`);
|
||||
expect(result).toContain('jsonSchemaValidator');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -315,4 +315,51 @@ describe('mock-paths transform', () => {
|
|||
expect(result).toContain('new MyError(');
|
||||
});
|
||||
});
|
||||
|
||||
describe('validator subpath rewrites', () => {
|
||||
it('rewrites vi.mock of validator provider to the subpath', () => {
|
||||
const input = [
|
||||
`vi.mock('@modelcontextprotocol/sdk/validation/cfworker-provider.js', () => ({`,
|
||||
` CfWorkerJsonSchemaValidator: vi.fn()`,
|
||||
`}));`,
|
||||
''
|
||||
].join('\n');
|
||||
const result = applyTransform(input);
|
||||
expect(result).toContain(`'@modelcontextprotocol/server/validators/cf-worker'`);
|
||||
expect(result).not.toContain('@modelcontextprotocol/sdk');
|
||||
});
|
||||
|
||||
it('rewrites vi.doMock of ajv provider with sibling client import', () => {
|
||||
const input = [
|
||||
`import { Client } from '@modelcontextprotocol/sdk/client/index.js';`,
|
||||
`vi.doMock('@modelcontextprotocol/sdk/validation/ajv-provider.js', () => ({`,
|
||||
` AjvJsonSchemaValidator: vi.fn()`,
|
||||
`}));`,
|
||||
''
|
||||
].join('\n');
|
||||
const result = applyTransform(input, { projectType: 'both' });
|
||||
expect(result).toContain(`'@modelcontextprotocol/client/validators/ajv'`);
|
||||
});
|
||||
|
||||
it('rewrites dynamic import of validator provider to the subpath', () => {
|
||||
const input = [
|
||||
`const { AjvJsonSchemaValidator } = await import('@modelcontextprotocol/sdk/validation/ajv-provider.js');`,
|
||||
''
|
||||
].join('\n');
|
||||
const result = applyTransform(input);
|
||||
expect(result).toContain(`'@modelcontextprotocol/server/validators/ajv'`);
|
||||
expect(result).toContain('AjvJsonSchemaValidator');
|
||||
});
|
||||
|
||||
it('rewrites jest.mock of validator short alias to the subpath', () => {
|
||||
const input = [
|
||||
`jest.mock('@modelcontextprotocol/sdk/validation/cfworker', () => ({`,
|
||||
` CfWorkerJsonSchemaValidator: jest.fn()`,
|
||||
`}));`,
|
||||
''
|
||||
].join('\n');
|
||||
const result = applyTransform(input);
|
||||
expect(result).toContain(`'@modelcontextprotocol/server/validators/cf-worker'`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue