From a30ef44abb573f407d8d05ac151e332eba98ea82 Mon Sep 17 00:00:00 2001 From: Jack Koppa Date: Tue, 11 Nov 2025 16:21:55 -0500 Subject: [PATCH 1/2] feat: Add Zod 4 support while maintaining Zod 3 compatibility - Move zod to peerDependencies supporting ^3.23.8 || ^4.0.0 - Create zodJsonSchema wrapper with auto-detection - Use native z.toJSONSchema() for Zod 4 - Fall back to zod-to-json-schema for Zod 3 - Add ZOD_4_MIGRATION.md documentation - Non-breaking change, fully backward compatible Closes #555 Amp-Thread-ID: https://ampcode.com/threads/T-6db05869-7946-4144-922f-b7adac392de5 Co-authored-by: Amp --- ZOD_4_MIGRATION.md | 103 ++++++++++++++++++++++++++++++++++++ package-lock.json | 15 ++++-- package.json | 13 +++-- src/server/mcp.ts | 2 +- src/server/zodJsonSchema.ts | 55 +++++++++++++++++++ 5 files changed, 177 insertions(+), 11 deletions(-) create mode 100644 ZOD_4_MIGRATION.md create mode 100644 src/server/zodJsonSchema.ts 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..58ae3edd2 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, 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); +} From 0b2ef19155dd979492009350102cbbb425d7d431 Mon Sep 17 00:00:00 2001 From: Jack Koppa Date: Tue, 11 Nov 2025 17:12:39 -0500 Subject: [PATCH 2/2] fix: Preserve ZodRawShape in RegisteredTool type The RegisteredTool type now preserves the original ZodRawShape instead of converting it to ZodType. This allows users to access shape properties directly, which is important for introspection and testing. The conversion to ZodType is now done only when needed: - During JSON schema generation (for API responses) - During validation (for parsing input/output) This is a non-breaking fix that maintains backward compatibility. --- src/server/mcp.ts | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/src/server/mcp.ts b/src/server/mcp.ts index 58ae3edd2..529d94663 100644 --- a/src/server/mcp.ts +++ b/src/server/mcp.ts @@ -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;