Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .changeset/cfworker-out-of-barrel.md
Original file line number Diff line number Diff line change
Expand Up @@ -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` 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), so consumers that don't opt into it no longer ship that code. The interim `@modelcontextprotocol/{server,client}/validators/cf-worker` subpath this introduced has been removed in a follow-up — the runtime shim is now the only entry point.
4 changes: 2 additions & 2 deletions .changeset/support-standard-json-schema.md
Original file line number Diff line number Diff line change
Expand Up @@ -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);
```

Expand Down
11 changes: 11 additions & 0 deletions .changeset/workerd-shim-vendors-cfworker.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
'@modelcontextprotocol/core': minor
'@modelcontextprotocol/client': patch
'@modelcontextprotocol/server': patch
---

Bundle automatic JSON Schema validator defaults in `@modelcontextprotocol/client` and `@modelcontextprotocol/server` runtime shims.

Client/server select defaults automatically based on the runtime: Node shims use AJV, while browser/workerd shims use `@cfworker/json-schema`. Those backends are bundled into the shim chunks that select them, so consumers do not need to install validator packages or import explicit validators for default behavior. Advanced users can still pass their own `jsonSchemaValidator` interface implementation.

The `@modelcontextprotocol/{client,server}/validators/cf-worker` subpath export has been removed — there is no longer any public entry point for the SDK's built-in validator classes. `AjvJsonSchemaValidator` and `CfWorkerJsonSchemaValidator` are now `@internal` and no longer exported from `@modelcontextprotocol/client` or `@modelcontextprotocol/server` (not even as types). The `jsonSchemaValidator` interface remains the public extension point for custom validators, and example JSDoc snippets no longer demonstrate direct validator instantiation.
13 changes: 7 additions & 6 deletions docs/migration-SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -500,8 +500,8 @@

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:

Expand All @@ -518,11 +518,12 @@
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`; client/server bundle the runtime-selected defaults.

Check warning on line 524 in docs/migration-SKILL.md

View check run for this annotation

Claude / Claude Code Review

cloudflareWorkers.test.ts still installs @cfworker/json-schema, masking the bundling regression this PR prevents

The migration docs added here say consumers should not install `@cfworker/json-schema`, but `test/integration/test/server/cloudflareWorkers.test.ts:46` — the only end-to-end test that runs the packed server tarball under real workerd — still pre-installs `'@cfworker/json-schema': '^4.1.1'` in the generated consumer package.json. That dep is now dead weight after this PR adds `@cfworker/json-schema` to `noExternal`, and it masks a future re-externalization regression because wrangler would resolv
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 The migration docs added here say consumers should not install @cfworker/json-schema, but test/integration/test/server/cloudflareWorkers.test.ts:46 — the only end-to-end test that runs the packed server tarball under real workerd — still pre-installs '@cfworker/json-schema': '^4.1.1' in the generated consumer package.json. That dep is now dead weight after this PR adds @cfworker/json-schema to noExternal, and it masks a future re-externalization regression because wrangler would resolve the bare import from the test's pre-installed copy instead of failing. Removing line 46 turns the test into a true regression guard and aligns it with the docs this PR adds.

Extended reasoning...

What the issue is

docs/migration-SKILL.md:524 (and the matching paragraph in docs/migration.md and .changeset/workerd-shim-vendors-cfworker.md) now instructs consumers: "Do not install ajv, ajv-formats, or @cfworker/json-schema; client/server bundle the runtime-selected defaults." But the SDK's own consumer simulation in test/integration/test/server/cloudflareWorkers.test.ts:44-47 still does exactly the thing the docs say not to do — it writes a generated package.json with an explicit dependency:

dependencies: {
    '@modelcontextprotocol/server': `file:./${tarballName}`,
    '@cfworker/json-schema': '^4.1.1'        // <-- now contradicts the docs
},

That file is not in this PR's diff (last touched in #1652), so this is a surviving instance of the pattern the PR replaces.

Why the explicit install used to be needed, and isn't anymore

At the PR base commit, packages/server/tsdown.config.ts had noExternal: ['@modelcontextprotocol/core'] only. The built dist/shimsWorkerd.mjs (and its chunks) therefore left a bare import { Validator } from '@cfworker/json-schema' for the consumer's bundler to resolve. wrangler's esbuild step would fail without it, so the test pre-installed the package.

This PR adds 'ajv', 'ajv-formats', '@cfworker/json-schema' to noExternal in both packages/server/tsdown.config.ts and packages/client/tsdown.config.ts, inlining the cfworker code into the workerd shim chunk. The explicit dep in the test consumer's package.json is now redundant.

Why it matters: the dead dep masks a regression

cloudflareWorkers.test.ts is the only test that exercises the packed tarball under a real workerd runtime via wrangler dev. The new barrelClean.test.ts tests added in this PR only string-match the dist/*.mjs output for bare from 'ajv'|'ajv-formats'|'@cfworker/json-schema' imports — useful but static. If a future tsdown/config change re-externalizes @cfworker/json-schema (e.g. someone removes it from noExternal while refactoring), the static test would catch it — but if that test is also touched, the workerd test should be the safety net. Today it isn't, because the consumer ships its own copy of @cfworker/json-schema, so wrangler's bundler would resolve the bare import from node_modules and the test would keep passing.

Step-by-step proof

  1. Test packs @modelcontextprotocol/server to a tarball and writes a consumer package.json listing both file:./<tarball> and '@cfworker/json-schema': '^4.1.1'.
  2. npm install puts both in the consumer's node_modules.
  3. The test's server.ts constructs new McpServer(...) — the Server constructor eagerly does this._jsonSchemaValidator = options?.jsonSchemaValidator ?? new DefaultJsonSchemaValidator(), so the workerd shim's static @cfworker/json-schema import path is exercised.
  4. wrangler dev runs esbuild over server.ts. After this PR, the import resolves to the bundled copy inside dist/shimsWorkerd.mjs — no bare import to satisfy.
  5. Hypothetical regression: @cfworker/json-schema is dropped from noExternal. dist/shimsWorkerd.mjs now has from '@cfworker/json-schema'. Without line 46, esbuild fails ("Could not resolve") and the test catches it. With line 46, esbuild resolves it from the test's pre-installed copy, the test passes, and the regression ships.

How to fix

Delete the '@cfworker/json-schema': '^4.1.1' line from the generated consumer package.json at test/integration/test/server/cloudflareWorkers.test.ts:46. This is a one-line change that turns the test into a genuine end-to-end regression guard for the bundling invariant and brings it into agreement with the docs this PR adds.

Filed as a nit: the test isn't directly modified by the PR, the static barrelClean.test.ts already guards the structural invariant, and removing the line is a hygiene improvement rather than a fix for broken behavior.

- The SDK's built-in validator classes (`AjvJsonSchemaValidator`, `CfWorkerJsonSchemaValidator`) are not exported from `@modelcontextprotocol/{client,server}` (not even as types). The `@modelcontextprotocol/{client,server}/validators/cf-worker` subpath that existed in v2 pre-releases has been removed.
- Advanced users may pass `jsonSchemaValidator: myCustomValidator` with their own implementation of the `jsonSchemaValidator` interface.

## 15. Migration Steps (apply in this order)

Expand Down
23 changes: 14 additions & 9 deletions docs/migration.md
Original file line number Diff line number Diff line change
Expand Up @@ -900,8 +900,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:

Expand Down Expand Up @@ -932,17 +932,22 @@ 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.

```typescript
// Runtime-aware default (auto-selects AjvJsonSchemaValidator or CfWorkerJsonSchemaValidator)
import { DefaultJsonSchemaValidator } from '@modelcontextprotocol/server/_shims';
Advanced users can still override validation by passing an object that implements the SDK's JSON Schema validator interface:

// Specific validators
import { AjvJsonSchemaValidator } from '@modelcontextprotocol/server';
import { CfWorkerJsonSchemaValidator } from '@modelcontextprotocol/server/validators/cf-worker';
```typescript
const server = new McpServer(
{ name: 'my-server', version: '1.0.0' },
{
capabilities: { tools: {} },
jsonSchemaValidator: myCustomValidator
}
);
```

The SDK's built-in validator classes (`AjvJsonSchemaValidator`, `CfWorkerJsonSchemaValidator`) are not part of the public surface — `@modelcontextprotocol/{client,server}` no longer export them as runtime values or types, and the `@modelcontextprotocol/{client,server}/validators/cf-worker` subpath that briefly existed in v2 pre-releases has been removed. To customise validation, implement the `jsonSchemaValidator` interface yourself and pass your implementation through the option above.

## Unchanged APIs

The following APIs are unchanged between v1 and v2 (only the import paths changed):
Expand Down
9 changes: 2 additions & 7 deletions packages/client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,6 @@
"types": "./dist/stdio.d.mts",
"import": "./dist/stdio.mjs"
},
"./validators/cf-worker": {
"types": "./dist/validators/cfWorker.d.mts",
"import": "./dist/validators/cfWorker.mjs"
},
"./_shims": {
"workerd": {
"types": "./dist/shimsWorkerd.d.mts",
Expand All @@ -54,9 +50,6 @@
"types": "./dist/index.d.mts",
"typesVersions": {
"*": {
"validators/cf-worker": [
"dist/validators/cfWorker.d.mts"
],
"stdio": [
"dist/stdio.d.mts"
]
Expand Down Expand Up @@ -93,6 +86,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",
Expand Down
2 changes: 1 addition & 1 deletion packages/client/src/client/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
2 changes: 1 addition & 1 deletion packages/client/src/shimsNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 —
Expand Down
10 changes: 0 additions & 10 deletions packages/client/src/validators/cfWorker.ts

This file was deleted.

14 changes: 14 additions & 0 deletions packages/client/test/client/barrelClean.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ 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/;
const VALIDATOR_BACKEND_IMPORT = /from\s+["'](?:ajv|ajv-formats|@cfworker\/json-schema)["']/;

function chunkImportsOf(entryPath: string): string[] {
const visited = new Set<string>();
Expand Down Expand Up @@ -52,4 +53,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 changes: 110 additions & 0 deletions packages/client/test/client/jsonSchemaValidatorOverride.test.ts
Original file line number Diff line number Diff line change
@@ -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 }]);
});
});
1 change: 1 addition & 0 deletions packages/client/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
],
Expand Down
14 changes: 10 additions & 4 deletions packages/client/tsdown.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ 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'],
entry: ['src/index.ts', 'src/stdio.ts', 'src/shimsNode.ts', 'src/shimsWorkerd.ts', 'src/shimsBrowser.ts'],

// 2. Output Configuration
format: ['esm'],
Expand All @@ -27,13 +27,19 @@ export default defineConfig({
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'],
// 5. Vendoring Strategy - Bundle this package's core implementation into the output,
// but treat most dependencies as external (require/import).
//
// The runtime `_shims` entries choose default JSON Schema validators: AJV on Node and
// @cfworker/json-schema on workerd/browser. Client users should not have to install a
// validator backend just to use the runtime default, so bundle the default backends into
// the shim chunks that select them.
noExternal: ['@modelcontextprotocol/core', 'ajv', 'ajv-formats', '@cfworker/json-schema'],

// 6. External packages - keep self-reference imports external for runtime resolution
external: ['@modelcontextprotocol/client/_shims']
Expand Down
16 changes: 14 additions & 2 deletions packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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
}
Expand All @@ -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",
Expand Down
Loading
Loading