feat(client,server): bundle default validators, expose customisation via subpaths (#2088)
This commit is contained in:
parent
4a5c863a21
commit
16d13abf78
46 changed files with 749 additions and 314 deletions
|
|
@ -3,4 +3,4 @@
|
|||
'@modelcontextprotocol/client': patch
|
||||
---
|
||||
|
||||
Stop bundling `@cfworker/json-schema` into the main package barrel. Previously `CfWorkerJsonSchemaValidator` was re-exported from the core internal barrel, so tsdown inlined the `@cfworker/json-schema` dev dependency into every consumer's bundle even when it was never used. The validator is now reachable only via the `_shims` conditional (workerd/browser) and the explicit `@modelcontextprotocol/{server,client}/validators/cf-worker` subpath, so consumers that don't opt into it no longer ship that code. No public API change.
|
||||
Stop bundling `@cfworker/json-schema` into the main package barrel. Previously `CfWorkerJsonSchemaValidator` was re-exported from the core internal barrel, so tsdown inlined the `@cfworker/json-schema` dependency into every consumer's bundle even when it was never used. The named validator classes are now reachable only via the explicit `@modelcontextprotocol/{client,server}/validators/{ajv,cf-worker}` subpaths and the runtime `_shims` conditional, so consumers that import only from the root entry point no longer ship the validator dep.
|
||||
|
|
|
|||
|
|
@ -21,10 +21,10 @@ server.registerTool('greet', {
|
|||
For raw JSON Schema (e.g. TypeBox output), use the new `fromJsonSchema` adapter:
|
||||
|
||||
```typescript
|
||||
import { fromJsonSchema, AjvJsonSchemaValidator } from '@modelcontextprotocol/core';
|
||||
import { fromJsonSchema } from '@modelcontextprotocol/server';
|
||||
|
||||
server.registerTool('greet', {
|
||||
inputSchema: fromJsonSchema({ type: 'object', properties: { name: { type: 'string' } } }, new AjvJsonSchemaValidator())
|
||||
inputSchema: fromJsonSchema({ type: 'object', properties: { name: { type: 'string' } } })
|
||||
}, handler);
|
||||
```
|
||||
|
||||
|
|
|
|||
18
.changeset/workerd-shim-vendors-cfworker.md
Normal file
18
.changeset/workerd-shim-vendors-cfworker.md
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
---
|
||||
'@modelcontextprotocol/core': minor
|
||||
'@modelcontextprotocol/client': patch
|
||||
'@modelcontextprotocol/server': patch
|
||||
---
|
||||
|
||||
Bundle automatic JSON Schema validator defaults in `@modelcontextprotocol/client` and `@modelcontextprotocol/server` runtime shims.
|
||||
|
||||
Client and server pick the right validator automatically based on the runtime: the Node shim uses AJV, the browser/workerd shim uses `@cfworker/json-schema`. Both backends are bundled into the shim chunks that select them, so the default code path needs no extra installs — `import { McpServer } from '@modelcontextprotocol/server'` does not pull `ajv` or `@cfworker/json-schema` into the root entry chunk.
|
||||
|
||||
The named validator classes remain part of the public surface for consumers who want to customize the built-in backend (pre-register schemas by `$id`, register custom AJV formats, switch dialects, change `@cfworker/json-schema` draft). They are exposed through explicit subpaths so they do not bloat the root index chunk:
|
||||
|
||||
- `import { AjvJsonSchemaValidator } from '@modelcontextprotocol/{client,server}/validators/ajv'`
|
||||
- `import { CfWorkerJsonSchemaValidator } from '@modelcontextprotocol/{client,server}/validators/cf-worker'`
|
||||
|
||||
Importing from one of these subpaths means the corresponding peer dep (`ajv` + `ajv-formats`, or `@cfworker/json-schema`) must be in your `package.json`. The shim keeps its own vendored copy for the default path, so a project can use the subpath in some files and rely on the default in others.
|
||||
|
||||
The `jsonSchemaValidator` interface remains the public extension point for replacing validation entirely with a custom implementation.
|
||||
|
|
@ -509,8 +509,8 @@ Type changes in handler context:
|
|||
|
||||
The SDK now auto-selects the appropriate JSON Schema validator based on runtime:
|
||||
|
||||
- Node.js → `AjvJsonSchemaValidator` (no change from v1)
|
||||
- Cloudflare Workers (workerd) → `CfWorkerJsonSchemaValidator` (previously required manual config)
|
||||
- Node.js → AJV (no change from v1)
|
||||
- Cloudflare Workers (workerd) → `@cfworker/json-schema` (previously required manual config)
|
||||
|
||||
**No action required** for most users. Cloudflare Workers users can remove explicit `jsonSchemaValidator` configuration:
|
||||
|
||||
|
|
@ -527,11 +527,12 @@ new McpServer(
|
|||
new McpServer({ name: 'server', version: '1.0.0' }, {});
|
||||
```
|
||||
|
||||
Access validators explicitly:
|
||||
Validator behavior:
|
||||
|
||||
- Runtime-aware default: `import { DefaultJsonSchemaValidator } from '@modelcontextprotocol/server/_shims';`
|
||||
- AJV (Node.js): `import { AjvJsonSchemaValidator } from '@modelcontextprotocol/server';`
|
||||
- CF Worker: `import { CfWorkerJsonSchemaValidator } from '@modelcontextprotocol/server/validators/cf-worker';`
|
||||
- Do not add validator imports for normal migrations.
|
||||
- Do not install `ajv`, `ajv-formats`, or `@cfworker/json-schema` for the default path; client/server bundle the runtime-selected defaults and the root entry point does not pull either dep in.
|
||||
- To customize the built-in backend (e.g. register custom AJV formats, change `@cfworker/json-schema` draft), import the named class from the package subpath: `@modelcontextprotocol/{client,server}/validators/ajv` for `AjvJsonSchemaValidator`, `@modelcontextprotocol/{client,server}/validators/cf-worker` for `CfWorkerJsonSchemaValidator`. Importing from a subpath means the corresponding peer dep must be in your `package.json`.
|
||||
- To replace validation entirely, pass `jsonSchemaValidator: myCustomValidator` with your own implementation of the `jsonSchemaValidator` interface.
|
||||
|
||||
## 15. Migration Steps (apply in this order)
|
||||
|
||||
|
|
|
|||
|
|
@ -901,8 +901,8 @@ server.setRequestHandler('tools/call', async (request, ctx) => {
|
|||
|
||||
The SDK now automatically selects the appropriate JSON Schema validator based on your runtime environment:
|
||||
|
||||
- **Node.js**: Uses `AjvJsonSchemaValidator` (same as v1 default)
|
||||
- **Cloudflare Workers**: Uses `CfWorkerJsonSchemaValidator` (previously required manual configuration)
|
||||
- **Node.js**: Uses AJV (same as v1 default)
|
||||
- **Cloudflare Workers**: Uses `@cfworker/json-schema` (previously required manual configuration)
|
||||
|
||||
This means Cloudflare Workers users no longer need to explicitly pass the validator:
|
||||
|
||||
|
|
@ -933,17 +933,45 @@ const server = new McpServer(
|
|||
);
|
||||
```
|
||||
|
||||
You can still explicitly override the validator if needed:
|
||||
You do not need to install or import validator packages for the default behavior. The client and server packages bundle the validator backend selected by the runtime shim, so a normal `import { McpServer } from '@modelcontextprotocol/server'` does not pull `ajv` or `@cfworker/json-schema` into your bundle until you choose to customize.
|
||||
|
||||
If you want to customize the **built-in** backend (for example, pre-register schemas by `$id`, register custom AJV formats, or change the `@cfworker/json-schema` draft), import the named class from the explicit subpath and pass an instance through `jsonSchemaValidator`:
|
||||
|
||||
```typescript
|
||||
// Runtime-aware default (auto-selects AjvJsonSchemaValidator or CfWorkerJsonSchemaValidator)
|
||||
import { DefaultJsonSchemaValidator } from '@modelcontextprotocol/server/_shims';
|
||||
import { Ajv } from 'ajv';
|
||||
import addFormats from 'ajv-formats';
|
||||
import { AjvJsonSchemaValidator } from '@modelcontextprotocol/server/validators/ajv';
|
||||
|
||||
// Specific validators
|
||||
import { AjvJsonSchemaValidator } from '@modelcontextprotocol/server';
|
||||
import { CfWorkerJsonSchemaValidator } from '@modelcontextprotocol/server/validators/cf-worker';
|
||||
const ajv = new Ajv({ strict: true, allErrors: true });
|
||||
addFormats(ajv);
|
||||
|
||||
const server = new McpServer(
|
||||
{ name: 'my-server', version: '1.0.0' },
|
||||
{
|
||||
capabilities: { tools: {} },
|
||||
jsonSchemaValidator: new AjvJsonSchemaValidator(ajv)
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
```typescript
|
||||
import { CfWorkerJsonSchemaValidator } from '@modelcontextprotocol/server/validators/cf-worker';
|
||||
|
||||
const server = new McpServer(
|
||||
{ name: 'my-server', version: '1.0.0' },
|
||||
{
|
||||
capabilities: { tools: {} },
|
||||
jsonSchemaValidator: new CfWorkerJsonSchemaValidator({ draft: '2020-12', shortcircuit: false })
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
(both subpaths are also available on `@modelcontextprotocol/client/validators/...`)
|
||||
|
||||
If you import from one of these subpaths in your own code, the corresponding peer dep (`ajv` + `ajv-formats`, or `@cfworker/json-schema`) needs to be installed in your `package.json`. The runtime shim continues to vendor a copy for the default code path, so you can use the subpath in some files and rely on the default in others.
|
||||
|
||||
To replace validation wholesale rather than customizing the built-in classes, implement the `jsonSchemaValidator` interface and pass your own implementation through the option above.
|
||||
|
||||
## Unchanged APIs
|
||||
|
||||
The following APIs are unchanged between v1 and v2 (only the import paths changed):
|
||||
|
|
|
|||
|
|
@ -28,6 +28,10 @@
|
|||
"types": "./dist/stdio.d.mts",
|
||||
"import": "./dist/stdio.mjs"
|
||||
},
|
||||
"./validators/ajv": {
|
||||
"types": "./dist/validators/ajv.d.mts",
|
||||
"import": "./dist/validators/ajv.mjs"
|
||||
},
|
||||
"./validators/cf-worker": {
|
||||
"types": "./dist/validators/cfWorker.d.mts",
|
||||
"import": "./dist/validators/cfWorker.mjs"
|
||||
|
|
@ -54,6 +58,9 @@
|
|||
"types": "./dist/index.d.mts",
|
||||
"typesVersions": {
|
||||
"*": {
|
||||
"validators/ajv": [
|
||||
"dist/validators/ajv.d.mts"
|
||||
],
|
||||
"validators/cf-worker": [
|
||||
"dist/validators/cfWorker.d.mts"
|
||||
],
|
||||
|
|
@ -93,6 +100,8 @@
|
|||
"@modelcontextprotocol/eslint-config": "workspace:^",
|
||||
"@modelcontextprotocol/test-helpers": "workspace:^",
|
||||
"@cfworker/json-schema": "catalog:runtimeShared",
|
||||
"ajv": "catalog:runtimeShared",
|
||||
"ajv-formats": "catalog:runtimeShared",
|
||||
"@types/content-type": "catalog:devTools",
|
||||
"@types/cross-spawn": "catalog:devTools",
|
||||
"@types/eventsource": "catalog:devTools",
|
||||
|
|
|
|||
|
|
@ -161,7 +161,7 @@ export type ClientOptions = ProtocolOptions & {
|
|||
* The validator is used to validate structured content returned by tools
|
||||
* against their declared output schemas.
|
||||
*
|
||||
* @default {@linkcode DefaultJsonSchemaValidator} ({@linkcode index.AjvJsonSchemaValidator | AjvJsonSchemaValidator} on Node.js, `CfWorkerJsonSchemaValidator` on Cloudflare Workers)
|
||||
* @default Runtime-selected validator (AJV-backed on Node.js, `@cfworker/json-schema`-backed on browser/workerd runtimes)
|
||||
*/
|
||||
jsonSchemaValidator?: jsonSchemaValidator;
|
||||
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
export { AjvJsonSchemaValidator as DefaultJsonSchemaValidator } from '@modelcontextprotocol/core/validators/ajv';
|
||||
|
||||
/**
|
||||
* Whether `fetch()` may throw `TypeError` due to CORS. CORS is a browser-only concept —
|
||||
|
|
|
|||
14
packages/client/src/validators/ajv.ts
Normal file
14
packages/client/src/validators/ajv.ts
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
/**
|
||||
* Customisation entry point for the AJV validator. Re-exports `Ajv` + `addFormats` from the
|
||||
* SDK's bundled copy, so customising the validator needs no extra installs.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* import { Ajv, addFormats, AjvJsonSchemaValidator } from '@modelcontextprotocol/client/validators/ajv';
|
||||
*
|
||||
* const ajv = new Ajv({ strict: true, allErrors: true });
|
||||
* addFormats(ajv);
|
||||
* const validator = new AjvJsonSchemaValidator(ajv);
|
||||
* ```
|
||||
*/
|
||||
export { addFormats, Ajv, AjvJsonSchemaValidator } from '@modelcontextprotocol/core/validators/ajv';
|
||||
|
|
@ -1,10 +1,3 @@
|
|||
/**
|
||||
* Cloudflare Workers JSON Schema validator, available as a sub-path export.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* import { CfWorkerJsonSchemaValidator } from '@modelcontextprotocol/client/validators/cf-worker';
|
||||
* ```
|
||||
*/
|
||||
/** Customisation entry point for the `@cfworker/json-schema` validator. */
|
||||
export type { CfWorkerSchemaDraft } from '@modelcontextprotocol/core/validators/cfWorker';
|
||||
export { CfWorkerJsonSchemaValidator } from '@modelcontextprotocol/core/validators/cfWorker';
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@ import { beforeAll, describe, expect, test } from 'vitest';
|
|||
const pkgDir = join(dirname(fileURLToPath(import.meta.url)), '../..');
|
||||
const distDir = join(pkgDir, 'dist');
|
||||
const NODE_ONLY = /\b(child_process|cross-spawn|node:stream|node:child_process)\b/;
|
||||
// Anchored at start-of-line so JSDoc-example `from 'ajv'` strings in vendored chunks don't match.
|
||||
const VALIDATOR_BACKEND_IMPORT = /^import[^\n]*?from\s+["'](?:ajv|ajv-formats|@cfworker\/json-schema)["']/m;
|
||||
|
||||
function chunkImportsOf(entryPath: string): string[] {
|
||||
const visited = new Set<string>();
|
||||
|
|
@ -52,4 +54,17 @@ describe('@modelcontextprotocol/client root entry is browser-safe', () => {
|
|||
expect(stdio).toMatch(/\bgetDefaultEnvironment\b/);
|
||||
expect(stdio).toMatch(/\bDEFAULT_INHERITED_ENV_VARS\b/);
|
||||
});
|
||||
|
||||
test('runtime shims vendor default validator backends instead of requiring consumers to install them', () => {
|
||||
for (const shim of ['shimsNode.mjs', 'shimsWorkerd.mjs', 'shimsBrowser.mjs']) {
|
||||
const entry = join(distDir, shim);
|
||||
expect(readFileSync(entry, 'utf8')).not.toMatch(VALIDATOR_BACKEND_IMPORT);
|
||||
|
||||
for (const chunk of chunkImportsOf(entry)) {
|
||||
expect({ chunk, content: readFileSync(chunk, 'utf8') }).not.toEqual(
|
||||
expect.objectContaining({ content: expect.stringMatching(VALIDATOR_BACKEND_IMPORT) })
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
|||
110
packages/client/test/client/jsonSchemaValidatorOverride.test.ts
Normal file
110
packages/client/test/client/jsonSchemaValidatorOverride.test.ts
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
import type { JSONRPCMessage, JsonSchemaType, JsonSchemaValidatorResult, jsonSchemaValidator } from '@modelcontextprotocol/core';
|
||||
import { InMemoryTransport, LATEST_PROTOCOL_VERSION } from '@modelcontextprotocol/core';
|
||||
import { Client } from '../../src/client/client.js';
|
||||
import { fromJsonSchema } from '../../src/fromJsonSchema.js';
|
||||
|
||||
class RecordingValidator implements jsonSchemaValidator {
|
||||
schemas: JsonSchemaType[] = [];
|
||||
values: unknown[] = [];
|
||||
|
||||
getValidator<T>(schema: JsonSchemaType) {
|
||||
this.schemas.push(schema);
|
||||
return (value: unknown): JsonSchemaValidatorResult<T> => {
|
||||
this.values.push(value);
|
||||
return { valid: true, data: value as T, errorMessage: undefined };
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async function connectInitializedClient(client: Client) {
|
||||
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
|
||||
serverTransport.onmessage = async message => {
|
||||
if ('method' in message && 'id' in message && message.method === 'initialize') {
|
||||
await serverTransport.send({
|
||||
jsonrpc: '2.0',
|
||||
id: message.id,
|
||||
result: {
|
||||
protocolVersion: LATEST_PROTOCOL_VERSION,
|
||||
capabilities: { tools: {} },
|
||||
serverInfo: { name: 'test-server', version: '1.0.0' }
|
||||
}
|
||||
});
|
||||
} else if ('method' in message && 'id' in message && message.method === 'tools/list') {
|
||||
await serverTransport.send({
|
||||
jsonrpc: '2.0',
|
||||
id: message.id,
|
||||
result: {
|
||||
tools: [
|
||||
{
|
||||
name: 'structured-tool',
|
||||
description: 'A tool with structured output',
|
||||
inputSchema: { type: 'object' },
|
||||
outputSchema: {
|
||||
type: 'object',
|
||||
properties: { count: { type: 'number' } },
|
||||
required: ['count']
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
} satisfies JSONRPCMessage);
|
||||
}
|
||||
};
|
||||
|
||||
await Promise.all([client.connect(clientTransport), serverTransport.start()]);
|
||||
return { clientTransport, serverTransport };
|
||||
}
|
||||
|
||||
describe('client JSON Schema validator overrides', () => {
|
||||
test('Client constructor uses a custom validator for tool output schema caching', async () => {
|
||||
const validator = new RecordingValidator();
|
||||
const client = new Client(
|
||||
{ name: 'test-client', version: '1.0.0' },
|
||||
{
|
||||
capabilities: {},
|
||||
jsonSchemaValidator: validator
|
||||
}
|
||||
);
|
||||
const { clientTransport, serverTransport } = await connectInitializedClient(client);
|
||||
|
||||
await expect(client.listTools()).resolves.toMatchObject({
|
||||
tools: [
|
||||
{
|
||||
name: 'structured-tool',
|
||||
outputSchema: {
|
||||
type: 'object',
|
||||
properties: { count: { type: 'number' } },
|
||||
required: ['count']
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
expect(validator.schemas).toEqual([
|
||||
{
|
||||
type: 'object',
|
||||
properties: { count: { type: 'number' } },
|
||||
required: ['count']
|
||||
}
|
||||
]);
|
||||
|
||||
await client.close();
|
||||
await clientTransport.close();
|
||||
await serverTransport.close();
|
||||
});
|
||||
|
||||
test('fromJsonSchema uses an explicitly supplied custom validator', async () => {
|
||||
const validator = new RecordingValidator();
|
||||
const schema: JsonSchemaType = {
|
||||
type: 'object',
|
||||
properties: { name: { type: 'string' } },
|
||||
required: ['name']
|
||||
};
|
||||
|
||||
const standardSchema = fromJsonSchema<{ name: string }>(schema, validator);
|
||||
expect(standardSchema['~standard'].validate({ name: 123 })).toEqual({ value: { name: 123 } });
|
||||
|
||||
expect(validator.schemas).toEqual([schema]);
|
||||
expect(validator.values).toEqual([{ name: 123 }]);
|
||||
});
|
||||
});
|
||||
|
|
@ -7,6 +7,7 @@
|
|||
"*": ["./*"],
|
||||
"@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"
|
||||
],
|
||||
|
|
|
|||
|
|
@ -2,39 +2,35 @@ import { defineConfig } from 'tsdown';
|
|||
|
||||
export default defineConfig({
|
||||
failOnWarn: 'ci-only',
|
||||
// 1. Entry Points
|
||||
// Directly matches package.json include/exclude globs
|
||||
entry: ['src/index.ts', 'src/stdio.ts', 'src/shimsNode.ts', 'src/shimsWorkerd.ts', 'src/shimsBrowser.ts', 'src/validators/cfWorker.ts'],
|
||||
|
||||
// 2. Output Configuration
|
||||
entry: [
|
||||
'src/index.ts',
|
||||
'src/stdio.ts',
|
||||
'src/shimsNode.ts',
|
||||
'src/shimsWorkerd.ts',
|
||||
'src/shimsBrowser.ts',
|
||||
'src/validators/ajv.ts',
|
||||
'src/validators/cfWorker.ts'
|
||||
],
|
||||
format: ['esm'],
|
||||
outDir: 'dist',
|
||||
clean: true, // Recommended: Cleans 'dist' before building
|
||||
clean: true,
|
||||
sourcemap: true,
|
||||
|
||||
// 3. Platform & Target
|
||||
target: 'esnext',
|
||||
platform: 'node',
|
||||
shims: true, // Polyfills common Node.js shims (__dirname, etc.)
|
||||
|
||||
// 4. Type Definitions
|
||||
// Bundles d.ts files into a single output
|
||||
shims: true,
|
||||
dts: {
|
||||
resolver: 'tsc',
|
||||
// override just for DTS generation:
|
||||
resolve: ['ajv', 'ajv-formats'],
|
||||
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']
|
||||
}
|
||||
}
|
||||
},
|
||||
// 5. Vendoring Strategy - Bundle the code for this specific package into the output,
|
||||
// but treat all other dependencies as external (require/import).
|
||||
noExternal: ['@modelcontextprotocol/core'],
|
||||
|
||||
// 6. External packages - keep self-reference imports external for runtime resolution
|
||||
noExternal: ['@modelcontextprotocol/core', 'ajv', 'ajv-formats', '@cfworker/json-schema'],
|
||||
external: ['@modelcontextprotocol/client/_shims']
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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'`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -33,6 +33,10 @@
|
|||
"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"
|
||||
|
|
@ -49,19 +53,25 @@
|
|||
"client": "tsx scripts/cli.ts client"
|
||||
},
|
||||
"dependencies": {
|
||||
"ajv": "catalog:runtimeShared",
|
||||
"ajv-formats": "catalog:runtimeShared",
|
||||
"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
|
||||
}
|
||||
|
|
@ -71,6 +81,8 @@
|
|||
"@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",
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue