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
6 changes: 6 additions & 0 deletions .changeset/node-codegen-fallback.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@modelcontextprotocol/server': patch
'@modelcontextprotocol/client': patch
---

On Node with code generation from strings disallowed (`--disallow-code-generation-from-strings`), the default JSON Schema validator is now the cf-worker validator. Ajv, the default otherwise, threw an `EvalError` on the first schema, so `fromJsonSchema` and the server and client constructors failed on such hosts.
22 changes: 21 additions & 1 deletion packages/client/src/shimsNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,27 @@
*
* This file is selected via package.json export conditions when running in Node.js.
*/
export { AjvJsonSchemaValidator as DefaultJsonSchemaValidator } from '@modelcontextprotocol/core-internal/validators/ajv';
import { AjvJsonSchemaValidator } from '@modelcontextprotocol/core-internal/validators/ajv';
import { CfWorkerJsonSchemaValidator } from '@modelcontextprotocol/core-internal/validators/cfWorker';

/**
* Ajv compiles validators with `new Function`. A Node process can disallow that
* (`--disallow-code-generation-from-strings`, or a host that sets it), and Ajv
* then throws an `EvalError` on the first schema. Such a process gets the
* validator the workerd and browser shims use, which does not generate code.
*/
function canGenerateCode(): boolean {
try {
new Function('');
return true;
} catch {
return false;
}
}

export const DefaultJsonSchemaValidator: typeof AjvJsonSchemaValidator | typeof CfWorkerJsonSchemaValidator = canGenerateCode()
? AjvJsonSchemaValidator
: CfWorkerJsonSchemaValidator;

/**
* Whether `fetch()` may throw `TypeError` due to CORS. CORS is a browser-only concept —
Expand Down
44 changes: 44 additions & 0 deletions packages/client/test/client/codegenDisallowed.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/**
* The node shim's default JSON Schema validator depends on whether the process
* may generate code from strings. A process started with
* `--disallow-code-generation-from-strings` throws an EvalError from
* `new Function`; the test stands that in by replacing the global `Function`
* while the shim module loads.
*/
import { afterEach, describe, expect, test, vi } from 'vitest';

const RealFunction = globalThis.Function;

async function loadShim(): Promise<typeof import('../../src/shimsNode')> {
vi.resetModules();
return import('../../src/shimsNode');
}

describe('node shim default JSON Schema validator', () => {
afterEach(() => {
globalThis.Function = RealFunction;
});

test('is Ajv when code generation is allowed', async () => {
const { DefaultJsonSchemaValidator } = await loadShim();
// Modules reload per test, so the classes are compared by name.
expect(DefaultJsonSchemaValidator.name).toBe('AjvJsonSchemaValidator');
});

test('is the cf-worker validator when code generation is disallowed, and it validates', async () => {
globalThis.Function = function () {
throw new EvalError('Code generation from strings disallowed for this context');
} as unknown as FunctionConstructor;
const { DefaultJsonSchemaValidator } = await loadShim();
globalThis.Function = RealFunction;

expect(DefaultJsonSchemaValidator.name).toBe('CfWorkerJsonSchemaValidator');
const check = new DefaultJsonSchemaValidator().getValidator<{ a: number }>({
type: 'object',
properties: { a: { type: 'number' } },
required: ['a']
});
expect(check({ a: 1 }).valid).toBe(true);
expect(check({ a: 'x' }).valid).toBe(false);
});
});
22 changes: 21 additions & 1 deletion packages/server/src/shimsNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,25 @@
*
* This file is selected via package.json export conditions when running in Node.js.
*/
export { AjvJsonSchemaValidator as DefaultJsonSchemaValidator } from '@modelcontextprotocol/core-internal/validators/ajv';
import { AjvJsonSchemaValidator } from '@modelcontextprotocol/core-internal/validators/ajv';
import { CfWorkerJsonSchemaValidator } from '@modelcontextprotocol/core-internal/validators/cfWorker';

/**
* Ajv compiles validators with `new Function`. A Node process can disallow that
* (`--disallow-code-generation-from-strings`, or a host that sets it), and Ajv
* then throws an `EvalError` on the first schema. Such a process gets the
* validator the workerd and browser shims use, which does not generate code.
*/
function canGenerateCode(): boolean {
try {
new Function('');
return true;
} catch {
return false;
}
}

export const DefaultJsonSchemaValidator: typeof AjvJsonSchemaValidator | typeof CfWorkerJsonSchemaValidator = canGenerateCode()
? AjvJsonSchemaValidator
: CfWorkerJsonSchemaValidator;
export { default as process } from 'node:process';
44 changes: 44 additions & 0 deletions packages/server/test/server/codegenDisallowed.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/**
* The node shim's default JSON Schema validator depends on whether the process
* may generate code from strings. A process started with
* `--disallow-code-generation-from-strings` throws an EvalError from
* `new Function`; the test stands that in by replacing the global `Function`
* while the shim module loads.
*/
import { afterEach, describe, expect, test, vi } from 'vitest';

const RealFunction = globalThis.Function;

async function loadShim(): Promise<typeof import('../../src/shimsNode')> {
vi.resetModules();
return import('../../src/shimsNode');
}

describe('node shim default JSON Schema validator', () => {
afterEach(() => {
globalThis.Function = RealFunction;
});

test('is Ajv when code generation is allowed', async () => {
const { DefaultJsonSchemaValidator } = await loadShim();
// Modules reload per test, so the classes are compared by name.
expect(DefaultJsonSchemaValidator.name).toBe('AjvJsonSchemaValidator');
});

test('is the cf-worker validator when code generation is disallowed, and it validates', async () => {
globalThis.Function = function () {
throw new EvalError('Code generation from strings disallowed for this context');
} as unknown as FunctionConstructor;
const { DefaultJsonSchemaValidator } = await loadShim();
globalThis.Function = RealFunction;

expect(DefaultJsonSchemaValidator.name).toBe('CfWorkerJsonSchemaValidator');
const check = new DefaultJsonSchemaValidator().getValidator<{ a: number }>({
type: 'object',
properties: { a: { type: 'number' } },
required: ['a']
});
expect(check({ a: 1 }).valid).toBe(true);
expect(check({ a: 'x' }).valid).toBe(false);
});
});
Loading