跳到主要内容

创建日期:2026-09-08 | 最近更新:2026-09-08 事实核对基于 zod 4.5.4@modelcontextprotocol/sdk 1.30.0(2026-07-27)在本机的实测与类型声明核对。

MCP 为什么“绕不开” Zod:把协议要的 JSON Schema 藏进作者体验

一句话:流传的「MCP 一定要用 zod」要对半看——协议本身只认 JSON Schema(语言无关),并不强制 zod;但 TypeScript 官方 SDK 把 zod 做成了工具参数的“作者语言”,于是你在 B 站/掘金看到的每个 MCP 教程里,工具参数都是 z.string() / z.number()。这一篇拆清楚:哪层用 zod、哪层不用、为什么偏偏是 zod,以及从 v3 迁 v4 要注意什么。

先接上一篇:Zod 入门 与本站的 MCP 栏目(协议本身怎么定义 tool)。

1. 协议层:只认 JSON Schema,不认 zod

MCP 里一个工具长这样(协议是语言无关的 JSON-RPC 风格):tools/list 时服务端返回每个工具的 inputSchematools/call 时客户端把 { name, arguments } 发给服务端。关键字段 inputSchema 的规范类型是 JSON Schema

{
"name": "web_search",
"description": "搜索网页",
"inputSchema": {
"type": "object",
"properties": {
"keyword": { "type": "string", "description": "搜索关键词" },
"limit": { "type": "integer", "minimum": 1, "maximum": 50, "default": 10 }
},
"required": ["keyword"]
}
}

所以严格来说,任何语言的 MCP 实现都不需要 zod——Python SDK 用的是自己的 pydantic/schema 思路,你只需要输出这份 JSON Schema。zod 不是协议的入场券。

2. 那 TS SDK 为什么绑 zod 绑得这么死

打开 @modelcontextprotocol/sdkpackage.json(1.30.0),依赖里赫然躺着 zod^3.25 || ^4.0)和 zod-to-json-schema。翻开它的类型声明,服务端注册工具时接受的就是一个 zod raw shape(内部叫 ZodRawShapeCompat,即 Record<string, zod schema>),并且会从 shape 里推断回调参数类型ShapeOutput<Args>):

import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { z } from 'zod';

const server = new McpServer({ name: 'search', version: '1.0.0' });

server.registerTool(
'web_search',
{
title: '网页搜索',
description: '搜索网页,可按需返回条数',
inputSchema: { // 传的就是 zod shape
keyword: z.string().describe('搜索关键词'),
limit: z.number().int().min(1).max(50).default(10).describe('返回条数上限'),
},
},
async ({ keyword, limit }) => { // keyword: string, limit: number —— 类型自动推断
// limit 已经被 zod 的 default 补齐过
return { content: [{ type: 'text', text: `${keyword} x${limit}` }] };
},
);

(老版本的 server.tool(name, description, paramsShape, cb) 写法已在 1.30 标记 deprecated,新 API 用 registerTool,但 schema 还是 zod。)

一个 zod 对象,在 SDK 里被用在了三个不同的地方,这正是「绕不开」的根本原因:

环节zod 干了什么换成别的东西要自己补什么
作者体验(开发时)zod raw shape 直接当参数表,且自动推断 handler 形参类型手写 JSON Schema 没类型;手写类型没法进运行时
上行(tools/list)把 shape 转成协议要的 inputSchema(JSON Schema)发给客户端/给模型看需要自己写 zod→JSON Schema 转换
下行(tools/call)收到 argumentssafeParse:校验、补 default、再交给你的回调需要自己写一套运行时校验

同一个 schema 描述了三件事——“一份声明,类型、协议、校验全齐”——这正是 Zod 入门 里那个心智模型的翻版,只是这次消费方是 MCP SDK。

3. 为什么偏偏是 zod(而不是 TypeBox / valibot / io-ts)

MCP 诞生在 2024 年底,TS 的 agent/AI 生态当时已经把 zod 当成了事实标准:tRPC、React Hook Form、AI SDK 全在它身上。官方 SDK 选 zod,等于选了「生态里开发者最熟、类型推导最顺、且有 zod-to-json-schema 这种成熟桥梁」的默认项——对教程和工具链的传染力是最重要的:官方示例用 zod,社区包(fastmcp 等)也跟着用 zod,最后所有 TS 教程都长成 z.string()

对比几个邻居(帮你选型,不是分高下):

特点在「MCP/agent 工具参数」语境
ZodTS 类型推断最强、生态最大、教程最多默认选择;官方 SDK 一等公民
TypeBoxschema 本身可 JSON 序列化、天生贴近 JSON Schema如果 schema 要跨语言/直接喂协议,参考本站 pi-ai 用它当工具 schema
valibot极小、零依赖、v3 也主打安全体积敏感时替换 zod,但生态和 SDK 默认不如 zod

一句话:MCP 需要 JSON Schema,zod 只是「最顺手的产出 JSON Schema 的方式之一」;之所以“看起来必须”,是官方 SDK 的默认 + 生态传染叠加出来的。

4. 实测:shape → inputSchema → safeParse

下面这段在 zod 4.5.4 上真实跑过——展示 SDK 内部那三个环节到底产出了什么:

import { z } from 'zod';

// ① registerTool 里那个 shape
const searchShape = z.object({
keyword: z.string().describe('搜索关键词'),
limit: z.number().int().min(1).max(50).default(10).describe('返回条数上限'),
});

// ② 上行:转成 tools/list 要发的 inputSchema
console.log(JSON.stringify(searchShape.toJSONSchema(), null, 2));

// ③ 下行:收到 arguments 后校验 + 补 default
const good = searchShape.safeParse({ keyword: 'mcp' }); // limit 缺省
const bad = searchShape.safeParse({ keyword: 'mcp', limit: 999 }); // 越界

实测输出:

② inputSchema:
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"keyword": { "type": "string", "description": "搜索关键词" },
"limit": { "default": 10, "type": "integer", "minimum": 1, "maximum": 50, "description": "返回条数上限" }
},
"required": ["keyword", "limit"],
"additionalProperties": false
}

③ 缺省参数 → {"keyword":"mcp","limit":10}
越界参数 → 被拒:✖ Too big: expected number to be <=50

两个值得注意的细节:

  • .describe('…') 会被原样写进 JSON Schema 的 description——也就是最终会发给 LLM 看。所以给工具参数写 zod .describe() 等于在写“给模型的工具说明书”,别只在代码注释里写一遍就完事。
  • zod v4 转出的对象默认 additionalProperties: false(JSON Schema 层面更严格);而 zod.object().parse() 默认是剥掉未知键而不是报错。两层严格度不一致,实践中靠把每个可能参数都显式声明.optional() / .default())来避免歧义,别赌客户端一定只发 shape 里的键。

5. 从 v3 到 v4,这里最容易踩的坑

  • SDK 同时兼容 zod 3 和 4:依赖写的是 ^3.25 || ^4.0,类型层有 zod/v3zod/v4/core 两套适配。你的项目跟 SDK 装到的 zod 主版本要一致,否则两个 z 副本会出现“shape 类型不认”的怪错。
  • 转换库换代:老教程/老 SDK 依赖 zod-to-json-schema,而它从 2025-11 起停止维护,官方建议直接用 Zod v4 原生 z.toJSONSchema()(实测输出 draft 2020-12)。SDK 1.30 仍带着旧依赖做兼容,但你在自己代码里转 schema 时,优先用 zod v4 原生 API。
  • z.enum / 顶层字符串格式:与 Zod v3→v4 速览(§10)一致——v4 推荐 z.email()z.uuid() 等顶层写法,老式 z.string().email() 也仍可用。

6. 那到底“什么时候必须用 zod”

只有一种情况称得上“必须”:

你用 TypeScript 官方 SDK 写 MCP server,且想要类型推断 + 自动转 JSON Schema + 自动校验的完整作者体验时——因为 SDK 把 zod 当成了唯一的一等 schema 语言,绕开它(手写 JSON Schema)等于放弃它替你做的类型与转换,还得自己跟 SDK 内部的 schema 语义对齐。

反过来,如果你写的是 Python/Go 客户端、或只有一份现成 JSON Schema、或在意跨语言 schema 复用,zod 都不是必需品——那是协议层“只要 JSON Schema”的功劳,也是它被设计成语言无关的原因。

关联

参考