Skip to content
Closed
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
103 changes: 103 additions & 0 deletions ZOD_4_MIGRATION.md
Original file line number Diff line number Diff line change
@@ -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)
15 changes: 10 additions & 5 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

13 changes: 8 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down Expand Up @@ -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"
Expand Down
28 changes: 18 additions & 10 deletions src/server/mcp.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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'])
Expand All @@ -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'];
Expand All @@ -144,7 +144,11 @@ export class McpServer {

if (tool.inputSchema) {
const cb = tool.callback as ToolCallback<ZodRawShape>;
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,
Expand All @@ -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,
Expand Down Expand Up @@ -671,8 +679,8 @@ export class McpServer {
const registeredTool: RegisteredTool = {
title,
description,
inputSchema: getZodSchemaObject(inputSchema),
outputSchema: getZodSchemaObject(outputSchema),
inputSchema,
outputSchema,
annotations,
_meta,
callback,
Expand All @@ -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;
Expand Down Expand Up @@ -1054,8 +1062,8 @@ export type ToolCallback<Args extends undefined | ZodRawShape | ZodType<object>
export type RegisteredTool = {
title?: string;
description?: string;
inputSchema?: ZodType<object>;
outputSchema?: ZodType<object>;
inputSchema?: ZodRawShape | ZodType<object>;
outputSchema?: ZodRawShape | ZodType<object>;
annotations?: ToolAnnotations;
_meta?: Record<string, unknown>;
callback: ToolCallback<undefined | ZodRawShape>;
Expand Down
55 changes: 55 additions & 0 deletions src/server/zodJsonSchema.ts
Original file line number Diff line number Diff line change
@@ -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);
}
Loading