diff --git a/ZOD_4_MIGRATION.md b/ZOD_4_MIGRATION.md new file mode 100644 index 000000000..a4fadeb87 --- /dev/null +++ b/ZOD_4_MIGRATION.md @@ -0,0 +1,103 @@ +# Zod 4 Support + +This SDK now supports both Zod 3 and Zod 4 seamlessly. + +## Installation + +### With Zod 3 (Current Default) + +```bash +npm install @modelcontextprotocol/sdk zod@^3.23.8 zod-to-json-schema@^3.24.1 +``` + +### With Zod 4 + +```bash +npm install @modelcontextprotocol/sdk zod@^4.0.0 +``` + +Note: `zod-to-json-schema` is **not needed** with Zod 4, as Zod 4 has native JSON Schema support via `z.toJSONSchema()`. + +## How It Works + +The SDK automatically detects which version of Zod you're using: + +- **Zod 4**: Uses the native `z.toJSONSchema()` function for optimal performance +- **Zod 3**: Falls back to the `zod-to-json-schema` library (must be installed) + +This is handled transparently by the SDK - you don't need to change your code when upgrading from Zod 3 to Zod 4. + +## Migration from Zod 3 to Zod 4 + +If you're currently using Zod 3 and want to upgrade to Zod 4: + +1. **Update your dependencies:** + + ```bash + npm install zod@^4.0.0 + npm uninstall zod-to-json-schema # Optional, no longer needed + ``` + +2. **Review Zod 4 breaking changes** that may affect your application code (not the MCP SDK itself): + - See [Zod 4 Migration Guide](https://zod.dev/v4) + - Most common changes: + - `z.string().email()` → `z.email()` (top-level function) + - `.default()` behavior changed (use `.prefault()` for old behavior) + - Error customization API changed (`message` → `error`) + +3. **Test your application** to ensure schema definitions work as expected + +## Compatibility Notes + +- The SDK maintains **full backwards compatibility** with Zod 3 +- You can upgrade to Zod 4 at your own pace +- Both versions are fully supported and tested + +## Examples + +Your existing code works with both versions: + +```typescript +import { McpServer } from '@modelcontextprotocol/sdk/server'; +import { z } from 'zod'; + +const server = new McpServer({ + name: 'example-server', + version: '1.0.0' +}); + +// This works with both Zod 3 and Zod 4 +server.tool( + 'greet', + 'Greets a person', + { + name: z.string(), + age: z.number().optional() + }, + async ({ name, age }) => ({ + content: [ + { + type: 'text', + text: `Hello ${name}${age ? `, you are ${age} years old` : ''}!` + } + ] + }) +); +``` + +## Troubleshooting + +### "zod-to-json-schema is required but not installed" + +If you see this error while using Zod 3, install the missing dependency: + +```bash +npm install zod-to-json-schema@^3.24.1 +``` + +This dependency is only needed for Zod 3. Zod 4 does not require it. + +## Version Support + +- **Zod 3**: `^3.23.8` (requires `zod-to-json-schema`) +- **Zod 4**: `^4.0.0` (native JSON Schema support) diff --git a/package-lock.json b/package-lock.json index b29ef11fd..1f227c041 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,9 +19,7 @@ "express": "^5.0.1", "express-rate-limit": "^7.5.0", "pkce-challenge": "^5.0.0", - "raw-body": "^3.0.0", - "zod": "^3.23.8", - "zod-to-json-schema": "^3.24.1" + "raw-body": "^3.0.0" }, "devDependencies": { "@cfworker/json-schema": "^4.1.1", @@ -47,13 +45,18 @@ "tsx": "^4.16.5", "typescript": "^5.5.4", "typescript-eslint": "^8.0.0", - "ws": "^8.18.0" + "ws": "^8.18.0", + "zod": "^3.23.8" }, "engines": { "node": ">=18" }, + "optionalDependencies": { + "zod-to-json-schema": "^3.24.1" + }, "peerDependencies": { - "@cfworker/json-schema": "^4.1.1" + "@cfworker/json-schema": "^4.1.1", + "zod": "^3.23.8 || ^4.0.0" }, "peerDependenciesMeta": { "@cfworker/json-schema": { @@ -6904,6 +6907,7 @@ "version": "3.24.1", "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.1.tgz", "integrity": "sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A==", + "devOptional": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" @@ -6914,6 +6918,7 @@ "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.1.tgz", "integrity": "sha512-3h08nf3Vw3Wl3PK+q3ow/lIil81IT2Oa7YpQyUUDsEWbXveMesdfK1xBd2RhCkynwZndAxixji/7SYJJowr62w==", "license": "ISC", + "optional": true, "peerDependencies": { "zod": "^3.24.1" } diff --git a/package.json b/package.json index 5c595515d..1df67907f 100644 --- a/package.json +++ b/package.json @@ -87,12 +87,14 @@ "express": "^5.0.1", "express-rate-limit": "^7.5.0", "pkce-challenge": "^5.0.0", - "raw-body": "^3.0.0", - "zod": "^3.23.8", - "zod-to-json-schema": "^3.24.1" + "raw-body": "^3.0.0" }, "peerDependencies": { - "@cfworker/json-schema": "^4.1.1" + "@cfworker/json-schema": "^4.1.1", + "zod": "^3.23.8 || ^4.0.0" + }, + "optionalDependencies": { + "zod-to-json-schema": "^3.24.1" }, "peerDependenciesMeta": { "@cfworker/json-schema": { @@ -123,7 +125,8 @@ "tsx": "^4.16.5", "typescript": "^5.5.4", "typescript-eslint": "^8.0.0", - "ws": "^8.18.0" + "ws": "^8.18.0", + "zod": "^3.23.8" }, "resolutions": { "strip-ansi": "6.0.1" diff --git a/src/server/mcp.ts b/src/server/mcp.ts index bee3b76ec..529d94663 100644 --- a/src/server/mcp.ts +++ b/src/server/mcp.ts @@ -1,5 +1,5 @@ import { Server, ServerOptions } from './index.js'; -import { zodToJsonSchema } from 'zod-to-json-schema'; +import { zodToJsonSchema } from './zodJsonSchema.js'; import { z, ZodRawShape, ZodObject, ZodString, ZodTypeAny, ZodType, ZodTypeDef, ZodOptional } from 'zod'; import { Implementation, @@ -107,7 +107,7 @@ export class McpServer { title: tool.title, description: tool.description, inputSchema: tool.inputSchema - ? (zodToJsonSchema(tool.inputSchema, { + ? (zodToJsonSchema(getZodSchemaObject(tool.inputSchema) ?? z.object({}), { strictUnions: true, pipeStrategy: 'input' }) as Tool['inputSchema']) @@ -117,7 +117,7 @@ export class McpServer { }; if (tool.outputSchema) { - toolDefinition.outputSchema = zodToJsonSchema(tool.outputSchema, { + toolDefinition.outputSchema = zodToJsonSchema(getZodSchemaObject(tool.outputSchema) ?? z.object({}), { strictUnions: true, pipeStrategy: 'output' }) as Tool['outputSchema']; @@ -144,7 +144,11 @@ export class McpServer { if (tool.inputSchema) { const cb = tool.callback as ToolCallback; - const parseResult = await tool.inputSchema.safeParseAsync(request.params.arguments); + const inputSchemaObject = getZodSchemaObject(tool.inputSchema); + if (!inputSchemaObject) { + throw new McpError(ErrorCode.InternalError, `Tool ${request.params.name} has invalid input schema`); + } + const parseResult = await inputSchemaObject.safeParseAsync(request.params.arguments); if (!parseResult.success) { throw new McpError( ErrorCode.InvalidParams, @@ -169,7 +173,11 @@ export class McpServer { } // if the tool has an output schema, validate structured content - const parseResult = await tool.outputSchema.safeParseAsync(result.structuredContent); + const outputSchemaObject = getZodSchemaObject(tool.outputSchema); + if (!outputSchemaObject) { + throw new McpError(ErrorCode.InternalError, `Tool ${request.params.name} has invalid output schema`); + } + const parseResult = await outputSchemaObject.safeParseAsync(result.structuredContent); if (!parseResult.success) { throw new McpError( ErrorCode.InvalidParams, @@ -671,8 +679,8 @@ export class McpServer { const registeredTool: RegisteredTool = { title, description, - inputSchema: getZodSchemaObject(inputSchema), - outputSchema: getZodSchemaObject(outputSchema), + inputSchema, + outputSchema, annotations, _meta, callback, @@ -690,7 +698,7 @@ export class McpServer { } if (typeof updates.title !== 'undefined') registeredTool.title = updates.title; if (typeof updates.description !== 'undefined') registeredTool.description = updates.description; - if (typeof updates.paramsSchema !== 'undefined') registeredTool.inputSchema = z.object(updates.paramsSchema); + if (typeof updates.paramsSchema !== 'undefined') registeredTool.inputSchema = updates.paramsSchema; if (typeof updates.callback !== 'undefined') registeredTool.callback = updates.callback; if (typeof updates.annotations !== 'undefined') registeredTool.annotations = updates.annotations; if (typeof updates._meta !== 'undefined') registeredTool._meta = updates._meta; @@ -1054,8 +1062,8 @@ export type ToolCallback export type RegisteredTool = { title?: string; description?: string; - inputSchema?: ZodType; - outputSchema?: ZodType; + inputSchema?: ZodRawShape | ZodType; + outputSchema?: ZodRawShape | ZodType; annotations?: ToolAnnotations; _meta?: Record; callback: ToolCallback; diff --git a/src/server/zodJsonSchema.ts b/src/server/zodJsonSchema.ts new file mode 100644 index 000000000..b796b7934 --- /dev/null +++ b/src/server/zodJsonSchema.ts @@ -0,0 +1,55 @@ +/** + * Compatibility wrapper for converting Zod schemas to JSON Schema. + * Supports both Zod 3 (via zod-to-json-schema) and Zod 4 (via native z.toJSONSchema). + */ + +import { ZodType } from 'zod'; + +// Store the imported function to avoid repeated dynamic imports +let zodToJsonSchemaFn: ((schema: ZodType, options?: { strictUnions?: boolean; pipeStrategy?: 'input' | 'output' }) => unknown) | null = + null; +let importAttempted = false; + +/** + * Converts a Zod schema to JSON Schema, supporting both Zod 3 and Zod 4. + */ +export function zodToJsonSchema(schema: ZodType, options?: { strictUnions?: boolean; pipeStrategy?: 'input' | 'output' }): unknown { + // Try Zod 4's native toJSONSchema first + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const z = schema.constructor as any; + if (z.toJSONSchema && typeof z.toJSONSchema === 'function') { + // Zod 4 native support + try { + return z.toJSONSchema(schema); + } catch { + // Fall through to zod-to-json-schema + } + } + + // Fall back to zod-to-json-schema for Zod 3 + if (!importAttempted) { + importAttempted = true; + try { + // Dynamic import for optional dependency - works in both ESM and CJS + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const zodToJsonSchemaModule = eval('require')('zod-to-json-schema'); + zodToJsonSchemaFn = + zodToJsonSchemaModule.zodToJsonSchema || zodToJsonSchemaModule.default?.zodToJsonSchema || zodToJsonSchemaModule.default; + } catch (e: unknown) { + const error = e as { code?: string; message?: string }; + if (error?.code === 'MODULE_NOT_FOUND' || error?.message?.includes('Cannot find module')) { + throw new Error( + 'zod-to-json-schema is required for Zod 3 support but is not installed. ' + + 'Please install it: npm install zod-to-json-schema' + ); + } + throw e; + } + } + + if (!zodToJsonSchemaFn) { + throw new Error('zod-to-json-schema module found but zodToJsonSchema function not available'); + } + + return zodToJsonSchemaFn(schema, options); +}