feat: auto-select JSON Schema validator based on runtime (#1456)

This commit is contained in:
Matt 2026-02-03 19:22:39 +00:00 • committed by GitHub
commit bc883afe3c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 1176 additions and 107 deletions

View file

@ -37,9 +37,11 @@
"@modelcontextprotocol/server": "workspace:^",
"@modelcontextprotocol/express": "workspace:^",
"@modelcontextprotocol/node": "workspace:^",
"@cfworker/json-schema": "catalog:runtimeShared",
"zod": "catalog:runtimeShared",
"vitest": "catalog:devTools",
"supertest": "catalog:devTools",
"wrangler": "catalog:devTools",
"@modelcontextprotocol/tsconfig": "workspace:^",
"@modelcontextprotocol/vitest-config": "workspace:^",
"@modelcontextprotocol/eslint-config": "workspace:^",

View file

@ -0,0 +1,163 @@
/**
* Cloudflare Workers integration test
*
* Verifies the MCP server package works in Cloudflare Workers
* WITHOUT nodejs_compat, using runtime shims for cross-platform compatibility.
*/
import type { ChildProcess } from 'node:child_process';
import { execSync, spawn } from 'node:child_process';
import * as fs from 'node:fs';
import * as os from 'node:os';
import path from 'node:path';
import { Client, StreamableHTTPClientTransport } from '@modelcontextprotocol/client';
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
const PORT = 8787;
interface TestEnv {
tempDir: string;
process: ChildProcess;
cleanup: () => Promise<void>;
}
describe('Cloudflare Workers compatibility (no nodejs_compat)', () => {
let env: TestEnv | null = null;
beforeAll(async () => {
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cf-worker-test-'));
// Pack server package
const serverPkgPath = path.resolve(__dirname, '../../../../packages/server');
const packOutput = execSync(`pnpm pack --pack-destination ${tempDir}`, {
cwd: serverPkgPath,
encoding: 'utf8'
});
const tarballName = path.basename(packOutput.trim().split('\n').pop()!);
// Write package.json
const pkgJson = {
name: 'cf-worker-test',
private: true,
type: 'module',
dependencies: {
'@modelcontextprotocol/server': `file:./${tarballName}`,
'@cfworker/json-schema': '^4.1.1'
},
devDependencies: {
wrangler: '^4.14.4'
}
};
fs.writeFileSync(path.join(tempDir, 'package.json'), JSON.stringify(pkgJson, null, 2));
// Write wrangler config
const wranglerConfig = {
$schema: 'node_modules/wrangler/config-schema.json',
name: 'cf-worker-test',
main: 'server.ts',
compatibility_date: '2025-01-01'
};
fs.writeFileSync(path.join(tempDir, 'wrangler.jsonc'), JSON.stringify(wranglerConfig, null, 2));
// Write server source
const serverSource = `
import { McpServer, WebStandardStreamableHTTPServerTransport } from '@modelcontextprotocol/server';
const server = new McpServer({ name: "test-server", version: "1.0.0" });
server.registerTool("greet", {
description: "Greet someone"
}, async (args) => ({
content: [{ type: "text", text: "Hello, " + (args.name || "World") + "!" }]
}));
const transport = new WebStandardStreamableHTTPServerTransport();
await server.connect(transport);
export default {
fetch: (request) => transport.handleRequest(request)
};
`;
fs.writeFileSync(path.join(tempDir, 'server.ts'), serverSource);
// Install dependencies
execSync('npm install', { cwd: tempDir, stdio: 'pipe', timeout: 60_000 });
// Start wrangler dev server
const proc = spawn('npx', ['wrangler', 'dev', '--local', '--port', String(PORT)], {
cwd: tempDir,
shell: true,
stdio: 'pipe'
});
// Wait for server to be ready
await new Promise<void>((resolve, reject) => {
const timeout = setTimeout(() => reject(new Error('Wrangler startup timeout')), 60_000);
let stderrData = '';
proc.stdout?.on('data', data => {
const output = data.toString();
if (/Ready on|Listening on/.test(output)) {
clearTimeout(timeout);
// Extra delay for wrangler to fully initialize
setTimeout(resolve, 1000);
}
});
proc.stderr?.on('data', data => {
stderrData += data.toString();
// Check for fatal errors like missing node: modules
if (/No such module "node:/.test(stderrData)) {
clearTimeout(timeout);
reject(new Error(`Wrangler fatal error: ${stderrData}`));
}
});
proc.on('error', err => {
clearTimeout(timeout);
reject(err);
});
proc.on('close', code => {
if (code !== 0 && code !== null) {
clearTimeout(timeout);
reject(new Error(`Wrangler exited with code ${code}. stderr: ${stderrData}`));
}
});
});
const cleanup = async () => {
proc.kill('SIGTERM');
await new Promise<void>(resolve => {
proc.on('close', () => resolve());
setTimeout(resolve, 5000);
});
try {
fs.rmSync(tempDir, { recursive: true, force: true });
} catch {
// Ignore cleanup errors
}
};
env = { tempDir, process: proc, cleanup };
}, 120_000);
afterAll(async () => {
await env?.cleanup();
});
it('should handle MCP requests', async () => {
expect(env).not.toBeNull();
const client = new Client({ name: 'test-client', version: '1.0.0' });
const transport = new StreamableHTTPClientTransport(new URL(`http://127.0.0.1:${PORT}/`));
await client.connect(transport);
const result = await client.callTool({ name: 'greet', arguments: { name: 'World' } });
expect(result.content).toEqual([{ type: 'text', text: 'Hello, World!' }]);
await client.close();
});
});

View file

@ -7,7 +7,9 @@
"*": ["./*"],
"@modelcontextprotocol/core": ["./node_modules/@modelcontextprotocol/core/src/index.ts"],
"@modelcontextprotocol/client": ["./node_modules/@modelcontextprotocol/client/src/index.ts"],
"@modelcontextprotocol/client/_shims": ["./node_modules/@modelcontextprotocol/client/src/shimsNode.ts"],
"@modelcontextprotocol/server": ["./node_modules/@modelcontextprotocol/server/src/index.ts"],
"@modelcontextprotocol/server/_shims": ["./node_modules/@modelcontextprotocol/server/src/shimsNode.ts"],
"@modelcontextprotocol/express": ["./node_modules/@modelcontextprotocol/express/src/index.ts"],
"@modelcontextprotocol/node": ["./node_modules/@modelcontextprotocol/node/src/index.ts"],
"@modelcontextprotocol/vitest-config": ["./node_modules/@modelcontextprotocol/vitest-config/tsconfig.json"],