feat!: drop Zod v3 support (#1460)

This commit is contained in:
Matt 2026-02-03 19:48:08 +00:00 • committed by GitHub
commit be5670f4fa
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
51 changed files with 669 additions and 947 deletions

View file

@ -88,12 +88,11 @@ Located in `packages/*/src/experimental/`:
- **Tasks**: Long-running task support with polling/resumption (`packages/core/src/experimental/tasks/`)
### Zod Compatibility
### Zod Schemas
The SDK uses `zod/v4` internally but supports both v3 and v4 APIs. Compatibility utilities:
The SDK uses `zod/v4` internally. Schema utilities live in:
- `packages/core/src/util/zod-compat.ts` - Schema parsing helpers that work across versions
- `packages/core/src/util/zod-json-schema-compat.ts` - Converts Zod schemas to JSON Schema
- `packages/core/src/util/schema.ts` - AnySchema alias and helpers for inspecting Zod objects
### Validation

View file

@ -39,7 +39,7 @@ This monorepo publishes split packages:
- **`@modelcontextprotocol/server`**: build MCP servers
- **`@modelcontextprotocol/client`**: build MCP clients
Both packages have a **required peer dependency** on `zod` for schema validation. The SDK internally imports from `zod/v4`, but remains compatible with projects using Zod v3.25+.
Both packages have a **required peer dependency** on `zod` for schema validation. The SDK uses Zod v4.
### Middleware packages (optional)

View file

@ -209,10 +209,12 @@ if (error instanceof OAuthError && error.code === OAuthErrorCode.InvalidClient)
The variadic `.tool()`, `.prompt()`, `.resource()` methods are removed. Use the `register*` methods with a config object.
**IMPORTANT**: v2 requires full Zod schemas — raw shapes like `{ name: z.string() }` are no longer supported. You must wrap with `z.object()`. This applies to `inputSchema`, `outputSchema`, and `argsSchema`.
### Tools
```typescript
// v1: server.tool(name, schema, callback)
// v1: server.tool(name, schema, callback) - raw shape worked
server.tool('greet', { name: z.string() }, async ({ name }) => {
return { content: [{ type: 'text', text: `Hello, ${name}!` }] };
});
@ -227,7 +229,7 @@ server.registerTool(
'greet',
{
description: 'Greet a user',
inputSchema: { name: z.string() }
inputSchema: z.object({ name: z.string() })
},
async ({ name }) => {
return { content: [{ type: 'text', text: `Hello, ${name}!` }] };
@ -240,7 +242,7 @@ Config object fields: `title?`, `description?`, `inputSchema?`, `outputSchema?`,
### Prompts
```typescript
// v1: server.prompt(name, schema, callback)
// v1: server.prompt(name, schema, callback) - raw shape worked
server.prompt('summarize', { text: z.string() }, async ({ text }) => {
return { messages: [{ role: 'user', content: { type: 'text', text } }] };
});
@ -249,7 +251,7 @@ server.prompt('summarize', { text: z.string() }, async ({ text }) => {
server.registerPrompt(
'summarize',
{
argsSchema: { text: z.string() }
argsSchema: z.object({ text: z.string() })
},
async ({ text }) => {
return { messages: [{ role: 'user', content: { type: 'text', text } }] };
@ -275,6 +277,15 @@ server.registerResource('config', 'config://app', {}, async uri => {
Note: the third argument (`metadata`) is required — pass `{}` if no metadata.
### Schema Migration Quick Reference
| v1 (raw shape) | v2 (Zod schema) |
|----------------|-----------------|
| `{ name: z.string() }` | `z.object({ name: z.string() })` |
| `{ count: z.number().optional() }` | `z.object({ count: z.number().optional() })` |
| `{}` (empty) | `z.object({})` |
| `undefined` (no schema) | `undefined` or omit the field |
## 7. Headers API
Transport constructors and `RequestInfo.headers` now use the Web Standard `Headers` object instead of plain objects.
@ -385,9 +396,10 @@ Access validators via `_shims` export: `import { DefaultJsonSchemaValidator } fr
2. Replace all imports from `@modelcontextprotocol/sdk/...` using the import mapping tables (sections 3-4), including `StreamableHTTPServerTransport` → `NodeStreamableHTTPServerTransport`
3. Replace removed type aliases (`JSONRPCError` → `JSONRPCErrorResponse`, etc.) per section 5
4. Replace `.tool()` / `.prompt()` / `.resource()` calls with `registerTool` / `registerPrompt` / `registerResource` per section 6
5. Replace plain header objects with `new Headers({...})` and bracket access (`headers['x']`) with `.get()` calls per section 7
6. If using `hostHeaderValidation` from server, update import and signature per section 8
7. If using server SSE transport, migrate to Streamable HTTP
8. If using server auth from the SDK, migrate to an external auth library
9. If relying on `listTools()`/`listPrompts()`/etc. throwing on missing capabilities, set `enforceStrictCapabilities: true`
10. Verify: build with `tsc` / run tests
5. **Wrap all raw Zod shapes with `z.object()`**: Change `inputSchema: { name: z.string() }` → `inputSchema: z.object({ name: z.string() })`. Same for `outputSchema` in tools and `argsSchema` in prompts.
6. Replace plain header objects with `new Headers({...})` and bracket access (`headers['x']`) with `.get()` calls per section 7
7. If using `hostHeaderValidation` from server, update import and signature per section 8
8. If using server SSE transport, migrate to Streamable HTTP
9. If using server auth from the SDK, migrate to an external auth library
10. If relying on `listTools()`/`listPrompts()`/etc. throwing on missing capabilities, set `enforceStrictCapabilities: true`
11. Verify: build with `tsc` / run tests

View file

@ -195,6 +195,7 @@ server.resource('config', 'config://app', async uri => {
```typescript
import { McpServer } from '@modelcontextprotocol/server';
import * as z from 'zod/v4';
const server = new McpServer({ name: 'demo', version: '1.0.0' });
@ -219,6 +220,42 @@ server.registerResource('config', 'config://app', {}, async uri => {
});
```
### Zod schemas required (raw shapes no longer supported)
v2 requires full Zod schemas for `inputSchema` and `argsSchema`. Raw object shapes are no longer accepted.
**Before (v1):**
```typescript
// Raw shape (object with Zod fields) - worked in v1
server.tool('greet', { name: z.string() }, async ({ name }) => { ... });
server.registerTool('greet', {
inputSchema: { name: z.string() } // raw shape
}, callback);
```
**After (v2):**
```typescript
import * as z from 'zod/v4';
// Must wrap with z.object()
server.registerTool('greet', {
inputSchema: z.object({ name: z.string() }) // full Zod schema
}, async ({ name }) => { ... });
// For tools with no parameters, use z.object({})
server.registerTool('ping', {
inputSchema: z.object({})
}, async () => { ... });
```
This applies to:
- `inputSchema` in `registerTool()`
- `outputSchema` in `registerTool()`
- `argsSchema` in `registerPrompt()`
### Host header validation moved
Express-specific middleware (`hostHeaderValidation()`, `localhostHostValidation()`) moved from the server package to `@modelcontextprotocol/express`. The server package now exports framework-agnostic functions instead: `validateHostHeader()`, `localhostAllowedHostnames()`,

View file

@ -368,7 +368,6 @@ class InteractiveOAuthClient {
name: toolName,
arguments: toolArgs
},
CallToolResultSchema,
{
task: {
taskId: `task-${Date.now()}`,

View file

@ -882,7 +882,6 @@ async function callToolTask(name: string, args: Record<string, unknown>): Promis
name,
arguments: args
},
CallToolResultSchema,
{
task: {
ttl: 60_000 // Keep results for 60 seconds

View file

@ -10,13 +10,7 @@
import { createInterface } from 'node:readline';
import type { CreateMessageRequest, CreateMessageResult, TextContent } from '@modelcontextprotocol/client';
import {
CallToolResultSchema,
Client,
ProtocolError,
ProtocolErrorCode,
StreamableHTTPClientTransport
} from '@modelcontextprotocol/client';
import { Client, ProtocolError, ProtocolErrorCode, StreamableHTTPClientTransport } from '@modelcontextprotocol/client';
// Create readline interface for user input
const readline = createInterface({
@ -127,7 +121,6 @@ async function run(url: string): Promise<void> {
const confirmStream = client.experimental.tasks.callToolStream(
{ name: 'confirm_delete', arguments: { filename: 'important.txt' } },
CallToolResultSchema,
{ task: { ttl: 60_000 } }
);
@ -158,10 +151,7 @@ async function run(url: string): Promise<void> {
const haikuStream = client.experimental.tasks.callToolStream(
{ name: 'write_haiku', arguments: { topic: 'autumn leaves' } },
CallToolResultSchema,
{
task: { ttl: 60_000 }
}
{ task: { ttl: 60_000 } }
);
for await (const message of haikuStream) {

View file

@ -31,8 +31,7 @@ server.registerTool(
'get-protocol-info',
{
title: 'Protocol Info',
description: 'Returns protocol version configuration',
inputSchema: {}
description: 'Returns protocol version configuration'
},
async (): Promise<CallToolResult> => ({
content: [

View file

@ -33,8 +33,7 @@ const mcpServer = new McpServer(
mcpServer.registerTool(
'register_user',
{
description: 'Register a new user account by collecting their information',
inputSchema: {}
description: 'Register a new user account by collecting their information'
},
async () => {
try {
@ -132,8 +131,7 @@ mcpServer.registerTool(
mcpServer.registerTool(
'create_event',
{
description: 'Create a calendar event by collecting event details',
inputSchema: {}
description: 'Create a calendar event by collecting event details'
},
async () => {
try {
@ -237,8 +235,7 @@ mcpServer.registerTool(
mcpServer.registerTool(
'update_shipping_address',
{
description: 'Update shipping address with validation',
inputSchema: {}
description: 'Update shipping address with validation'
},
async () => {
try {

View file

@ -22,7 +22,7 @@ import { isInitializeRequest, McpServer, UrlElicitationRequiredError } from '@mo
import cors from 'cors';
import type { Request, Response } from 'express';
import express from 'express';
import { z } from 'zod';
import * as z from 'zod/v4';
import { InMemoryEventStore } from './inMemoryEventStore.js';
@ -42,9 +42,9 @@ const getServer = () => {
'payment-confirm',
{
description: 'A tool that confirms a payment directly with a user',
inputSchema: {
inputSchema: z.object({
cartId: z.string().describe('The ID of the cart to confirm')
}
})
},
async ({ cartId }, extra): Promise<CallToolResult> => {
/*
@ -75,9 +75,9 @@ const getServer = () => {
'third-party-auth',
{
description: 'A demo tool that requires third-party OAuth credentials',
inputSchema: {
inputSchema: z.object({
param1: z.string().describe('First parameter')
}
})
},
async (_, extra): Promise<CallToolResult> => {
/*

View file

@ -26,7 +26,7 @@ server.registerTool(
{
title: 'Greeting Tool',
description: 'A simple greeting tool',
inputSchema: { name: z.string().describe('Name to greet') }
inputSchema: z.object({ name: z.string().describe('Name to greet') })
},
async ({ name }): Promise<CallToolResult> => {
return {

View file

@ -26,9 +26,9 @@ const getServer = () => {
'greet',
{
description: 'A simple greeting tool',
inputSchema: {
inputSchema: z.object({
name: z.string().describe('Name to greet')
}
})
},
async ({ name }): Promise<CallToolResult> => {
return {
@ -47,9 +47,9 @@ const getServer = () => {
'multi-greet',
{
description: 'A tool that sends different greetings with delays between them',
inputSchema: {
inputSchema: z.object({
name: z.string().describe('Name to greet')
}
})
},
async ({ name }, extra): Promise<CallToolResult> => {
const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));

View file

@ -17,11 +17,11 @@ server.registerTool(
'get_weather',
{
description: 'Get weather information for a city',
inputSchema: {
inputSchema: z.object({
city: z.string().describe('City name'),
country: z.string().describe('Country code (e.g., US, UK)')
},
outputSchema: {
}),
outputSchema: z.object({
temperature: z.object({
celsius: z.number(),
fahrenheit: z.number()
@ -32,7 +32,7 @@ server.registerTool(
speed_kmh: z.number(),
direction: z.string()
})
}
})
},
async ({ city, country }) => {
// Parameters are available but not used in this example

View file

@ -20,9 +20,9 @@ const getServer = () => {
'greeting-template',
{
description: 'A simple greeting prompt template',
argsSchema: {
argsSchema: z.object({
name: z.string().describe('Name to include in greeting')
}
})
},
async ({ name }): Promise<GetPromptResult> => {
return {
@ -44,10 +44,10 @@ const getServer = () => {
'start-notification-stream',
{
description: 'Starts sending periodic notifications for testing resumability',
inputSchema: {
inputSchema: z.object({
interval: z.number().describe('Interval in milliseconds between notifications').default(100),
count: z.number().describe('Number of notifications to send (0 for 100)').default(10)
}
})
},
async ({ interval, count }, extra): Promise<CallToolResult> => {
const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));

View file

@ -58,9 +58,9 @@ const getServer = () => {
{
title: 'Greeting Tool', // Display name for UI
description: 'A simple greeting tool',
inputSchema: {
inputSchema: z.object({
name: z.string().describe('Name to greet')
}
})
},
async ({ name }): Promise<CallToolResult> => {
return {
@ -79,9 +79,9 @@ const getServer = () => {
'multi-greet',
{
description: 'A tool that sends different greetings with delays between them',
inputSchema: {
inputSchema: z.object({
name: z.string().describe('Name to greet')
},
}),
annotations: {
title: 'Multiple Greeting Tool',
readOnlyHint: true,
@ -135,9 +135,9 @@ const getServer = () => {
'collect-user-info',
{
description: 'A tool that collects user information through form elicitation',
inputSchema: {
inputSchema: z.object({
infoType: z.enum(['contact', 'preferences', 'feedback']).describe('Type of information to collect')
}
})
},
async ({ infoType }, extra): Promise<CallToolResult> => {
let message: string;
@ -298,9 +298,9 @@ const getServer = () => {
{
title: 'Greeting Template', // Display name for UI
description: 'A simple greeting prompt template',
argsSchema: {
argsSchema: z.object({
name: z.string().describe('Name to include in greeting')
}
})
},
async ({ name }): Promise<GetPromptResult> => {
return {
@ -322,10 +322,10 @@ const getServer = () => {
'start-notification-stream',
{
description: 'Starts sending periodic notifications for testing resumability',
inputSchema: {
inputSchema: z.object({
interval: z.number().describe('Interval in milliseconds between notifications').default(100),
count: z.number().describe('Number of notifications to send (0 for 100)').default(50)
}
})
},
async ({ interval, count }, extra): Promise<CallToolResult> => {
const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
@ -427,9 +427,9 @@ const getServer = () => {
{
title: 'List Files with ResourceLinks',
description: 'Returns a list of files as ResourceLinks without embedding their content',
inputSchema: {
inputSchema: z.object({
includeDescriptions: z.boolean().optional().describe('Whether to include descriptions in the resource links')
}
})
},
async ({ includeDescriptions = true }): Promise<CallToolResult> => {
const resourceLinks: ResourceLink[] = [
@ -479,9 +479,9 @@ const getServer = () => {
{
title: 'Delay',
description: 'A simple tool that delays for a specified duration, useful for testing task execution',
inputSchema: {
inputSchema: z.object({
duration: z.number().describe('Duration in milliseconds').default(5000)
}
})
},
{
async createTask({ duration }, { taskStore, taskRequestedTtl }) {

View file

@ -13,9 +13,9 @@ mcpServer.registerTool(
'summarize',
{
description: 'Summarize any text using an LLM',
inputSchema: {
inputSchema: z.object({
text: z.string().describe('Text to summarize')
}
})
},
async ({ text }) => {
// Call the LLM through MCP sampling

View file

@ -56,11 +56,11 @@ import {
ListResourceTemplatesResultSchema,
ListToolsResultSchema,
mergeCapabilities,
parseSchema,
Protocol,
ProtocolError,
ProtocolErrorCode,
ReadResourceResultSchema,
safeParse,
SdkError,
SdkErrorCode
} from '@modelcontextprotocol/core';
@ -320,7 +320,7 @@ export class Client<
request: RequestTypeMap[M],
extra: RequestHandlerExtra<ClientRequest | RequestT, ClientNotification | NotificationT>
): Promise<ClientResult | ResultT> => {
const validatedRequest = safeParse(ElicitRequestSchema, request);
const validatedRequest = parseSchema(ElicitRequestSchema, request);
if (!validatedRequest.success) {
// Type guard: if success is false, error is guaranteed to exist
const errorMessage =
@ -344,7 +344,7 @@ export class Client<
// When task creation is requested, validate and return CreateTaskResult
if (params.task) {
const taskValidationResult = safeParse(CreateTaskResultSchema, result);
const taskValidationResult = parseSchema(CreateTaskResultSchema, result);
if (!taskValidationResult.success) {
const errorMessage =
taskValidationResult.error instanceof Error
@ -356,7 +356,7 @@ export class Client<
}
// For non-task requests, validate against ElicitResultSchema
const validationResult = safeParse(ElicitResultSchema, result);
const validationResult = parseSchema(ElicitResultSchema, result);
if (!validationResult.success) {
// Type guard: if success is false, error is guaranteed to exist
const errorMessage =
@ -393,7 +393,7 @@ export class Client<
request: RequestTypeMap[M],
extra: RequestHandlerExtra<ClientRequest | RequestT, ClientNotification | NotificationT>
): Promise<ClientResult | ResultT> => {
const validatedRequest = safeParse(CreateMessageRequestSchema, request);
const validatedRequest = parseSchema(CreateMessageRequestSchema, request);
if (!validatedRequest.success) {
const errorMessage =
validatedRequest.error instanceof Error ? validatedRequest.error.message : String(validatedRequest.error);
@ -406,7 +406,7 @@ export class Client<
// When task creation is requested, validate and return CreateTaskResult
if (params.task) {
const taskValidationResult = safeParse(CreateTaskResultSchema, result);
const taskValidationResult = parseSchema(CreateTaskResultSchema, result);
if (!taskValidationResult.success) {
const errorMessage =
taskValidationResult.error instanceof Error
@ -420,7 +420,7 @@ export class Client<
// For non-task requests, validate against appropriate schema based on tools presence
const hasTools = params.tools || params.toolChoice;
const resultSchema = hasTools ? CreateMessageResultWithToolsSchema : CreateMessageResultSchema;
const validationResult = safeParse(resultSchema, result);
const validationResult = parseSchema(resultSchema, result);
if (!validationResult.success) {
const errorMessage =
validationResult.error instanceof Error ? validationResult.error.message : String(validationResult.error);
@ -873,7 +873,7 @@ export class Client<
fetcher: () => Promise<T[]>
): void {
// Validate options using Zod schema (validates autoRefresh and debounceMs)
const parseResult = ListChangedOptionsBaseSchema.safeParse(options);
const parseResult = parseSchema(ListChangedOptionsBaseSchema, options);
if (!parseResult.success) {
throw new Error(`Invalid ${listType} listChanged options: ${parseResult.error.message}`);
}

View file

@ -10,7 +10,6 @@ import type {
CallToolRequest,
CancelTaskResult,
ClientRequest,
CompatibilityCallToolResultSchema,
GetTaskResult,
ListTasksResult,
Notification,
@ -92,11 +91,10 @@ export class ExperimentalClientTasks<
*
* @experimental
*/
async *callToolStream<T extends typeof CallToolResultSchema | typeof CompatibilityCallToolResultSchema>(
async *callToolStream(
params: CallToolRequest['params'],
resultSchema: T = CallToolResultSchema as T,
options?: RequestOptions
): AsyncGenerator<ResponseMessage<SchemaOutput<T>>, void, void> {
): AsyncGenerator<ResponseMessage<SchemaOutput<typeof CallToolResultSchema>>, void, void> {
// Access Client's internal methods
const clientInternal = this._client as unknown as ClientInternal<RequestT>;
@ -108,7 +106,7 @@ export class ExperimentalClientTasks<
task: options?.task ?? (clientInternal.isToolTask(params.name) ? {} : undefined)
};
const stream = clientInternal.requestStream({ method: 'tools/call', params }, resultSchema, optionsWithTask);
const stream = clientInternal.requestStream({ method: 'tools/call', params }, CallToolResultSchema, optionsWithTask);
// Get the validator for this tool (if it has an output schema)
const validator = clientInternal.getToolOutputValidator(params.name);

View file

@ -37,8 +37,7 @@
"ajv": "catalog:runtimeShared",
"ajv-formats": "catalog:runtimeShared",
"json-schema-typed": "catalog:runtimeShared",
"zod": "catalog:runtimeShared",
"zod-to-json-schema": "catalog:runtimeShared"
"zod": "catalog:runtimeShared"
},
"peerDependencies": {
"@cfworker/json-schema": "catalog:runtimeShared",

View file

@ -11,8 +11,7 @@ export * from './shared/transport.js';
export * from './shared/uriTemplate.js';
export * from './types/types.js';
export * from './util/inMemory.js';
export * from './util/zodCompat.js';
export * from './util/zodJsonSchemaCompat.js';
export * from './util/schema.js';
// experimental exports
export * from './experimental/index.js';

Some files were not shown because too many files have changed in this diff Show more