跳到主要内容

高层服务器 API:McpServer

本章讲:你调 server.registerTool(...) 之后,SDK 内部到底发生了什么——一个声明式注册项怎么变成能响应 tools/list / tools/call 的处理器,以及输入输出是怎么被校验的。

1. 它要解决的小问题

协议层(第 2 章)只认 JSON-RPC 方法名:客户端发 tools/list,你得返回工具清单;发 tools/call,你得跑对应函数。手写这些处理器很繁琐——而且 MCP 规范有不少硬性要求(声明了某能力就必须响应它的 list 方法,即使是空结果)。

McpServerServer(底层)之上的便利层:你只声明「有哪些工具/资源/提示词」,它替你装好所有处理器。

2. 思路/直觉:注册表 + 懒加载处理器

核心数据结构是四张「注册表」字典(uri/name → 注册项):

// packages/server/src/server/mcp.ts:72-77 —— 四张注册表
private _registeredResources: { [uri: string]: RegisteredResource } = {};
private _registeredResourceTemplates: { [name: string]: RegisteredResourceTemplate } = {};
private _registeredTools: { [name: string]: RegisteredTool } = {};
private _registeredPrompts: { [name: string]: RegisteredPrompt } = {};

处理器懒加载:第一次注册某类原语时,才把对应的 JSON-RPC 处理器装到底层 Server 上,并用一个 _toolHandlersInitialized 之类的标志位防止重复装。见 setToolRequestHandlers(mcp.ts:161)开头的幂等守卫。

规范坑(已处理): 如果你在 new McpServer({...}, { capabilities: { tools: {} } })预先声明了 tools 能力,构造函数会立即装处理器(mcp.ts:125),哪怕你还没注册任何工具。原因:规范要求「声明了能力就必须响应其 list 方法」,否则会回 −32601 Method not found,违反规范。

3. 主线:一次 tools/call 在 McpServer 里的五步

这张图是 tools/call 处理器(mcp.ts:210)的内部流程,从左到右,任一步失败即转错误分支。

请求到达


① 查注册表 ──未找到/已禁用──▶ 抛 ProtocolError(InvalidParams)
│ 找到

② validateToolInput ──校验失败──▶ 抛 InvalidParams
│ 通过(得到类型化 args)

③ executeToolHandler ──你的函数跑这里──▶ 结果


④ validateToolOutput ──有 outputSchema 但无 structuredContent──▶ 抛 InvalidParams
│ 通过

⑤ projectCallToolResult(交给 wire codec 做纪元投影)──▶ 返回

对应真实代码的骨架(已精简):

// 示意,浓缩自 packages/server/src/server/mcp.ts:210-236
this.server.setRequestHandler('tools/call', async (request, ctx) => {
const tool = this._registeredTools[request.params.name];
if (!tool) throw new ProtocolError(InvalidParams, `Tool ... not found`); // ①
try {
const args = await this.validateToolInput(tool, request.params.arguments, name); // ②
const result = await this.executeToolHandler(tool, args, ctx); // ③
await this.validateToolOutput(tool, result, name); // ④
if (isInputRequiredResult(result)) return result; // 多轮交互结果,原样透传
return this.server.projectCallToolResult(result, tool.outputSchemaJson); // ⑤
} catch (error) {
return this.createToolError(...); // 工具内部错误 → isError:true 的结果,不是协议错误
}
});

一个重要区分: 工具业务失败(catch 分支)被包成 { content:[...], isError: true }正常结果(createToolError,mcp.ts:247),而不是 JSON-RPC 协议错误。只有「工具不存在/参数非法」这种协议级问题才抛 ProtocolError。这让 LLM 能看到工具的错误文本并自我纠正。

4. 核心机制

4.1 输入校验:Standard Schema,不绑死 Zod

v2 不再硬依赖 Zod。inputSchema 接受任何实现了 Standard Schema 的库(Zod v4 / Valibot / ArkType)。校验走统一入口 validateStandardSchema:

// packages/server/src/server/mcp.ts:274-280 —— validateToolInput 核心
const parseResult = await validateStandardSchema(tool.inputSchema, args ?? {});
if (!parseResult.success) {
throw new ProtocolError(InvalidParams, `Input validation error: ...: ${parseResult.error}`);
}
return parseResult.data as ...; // 处理器拿到的是「已校验、已转型」的 args

registerTool 还保留一个 @deprecated 的「raw shape」重载:你传 { field: z.string() } 这种裸记录,它用 normalizeRawShapeSchema(mcp.ts:995)自动包成 z.object(...)。这是为兼容 v1 写法。

4.2 输出校验:structuredContent 的「存在性」判断很讲究

如果工具声明了 outputSchema,SDK 会校验 result.structuredContent关键细节:判断「有没有结构化内容」用的是 === undefined,不是真值判断:

// packages/server/src/server/mcp.ts:307-312 —— 为何不能用 falsy 判断
if (result.structuredContent === undefined) {
throw new ProtocolError(InvalidParams, `... has an output schema but no structured content ...`);
}

原因(注释里写得很清楚):structuredContent 合法地可以是 null0false""——这些都是有效 JSON 值。若用 if (!structuredContent) 就会把合法的 0 当成「缺失」。存在时一律送进 schema 校验。

4.3 tools/list 的 JSON Schema 转换 + 记忆化

tools/list 处理器(mcp.ts:178)把每个工具的 inputSchema(Standard Schema)转成 JSON Schema(standardSchemaToJsonSchema),因为线上传的是 JSON Schema 而非 Zod 对象。

这个转换记忆化_toolInputSchemaJson(mcp.ts:84)。为什么?因为 HTTP 入口用「每请求一个工厂」模型(见 createMcpHandler),同一个工具可能被反复转换;缓存让注册时的转换和分发前的 SEP-2243 头校验共用一次成果。

注意 registerTool 里这段转换是 「warn, never throw」(mcp.ts:821-836):转换失败只警告、不抛——这样本地对着「忽略该字段的 stdio 客户端」开发时不会被阻塞。

4.4 资源:静态 URI vs 模板

registerResource 有两个重载(mcp.ts:580/586):

形态第二参数解析方式读回调签名
静态资源URI 字符串字典精确匹配(uri, ctx)
模板资源ResourceTemplateURI 模板 match() 抽变量(uri, variables, ctx)

resources/read 处理器(mcp.ts:478)先查静态资源精确匹配,再遍历模板做 uriTemplate.match(),都没命中就抛 ResourceNotFoundError(mcp.ts:504)——一个中性领域错误,具体的线上错误码(每个纪元都是 −32602)由 wire codec 的 encodeErrorCode 决定。这是「中性领域层 + 纪元感知编码」分工的典型例子。

4.5 动态增删:update / enable / disable / remove

每个注册项返回一个带 update/enable/disable/remove 的句柄(如 RegisteredTool,mcp.ts:1256)。改完会自动发 notifications/tools/list_changed 通知客户端刷新。

update 里藏着不少边界处理,例如重命名工具时要同步驱逐两份记忆化缓存——旧名一份、还可能撞上的目标名一份(rename 没有重名守卫),否则 SEP-2243 预分发校验会拿到错误的 schema(mcp.ts:863-879)。

5. 巧妙之处

  • 中性结果 + 纪元投影分离: tools/call 处理器本身纪元无关,只产出中性结果;projectCallToolResult(交给 wire codec)负责 SEP-2106 的两件事:任意纪元都做的 TextContent 自动追加,以及仅 2025 纪元的 {result:…} 包裹。处理器永远不碰这些差异(mcp.ts:229,契约见 wire/codec.ts:243)。

  • input_required 早返回: 工具处理器可以返回一个「需要更多输入」的结果(多轮交互,MRTR)。tools/call 在输出校验之前就用 isInputRequiredResult 拦截并原样透传(mcp.ts:223),因为它不是最终输出,不该跑 outputSchema 校验。

6. 边界与局限

  • raw-shape 注册(裸 {...} 而非 z.object)只对 Zod 有效——z.object() 包不了别的 Standard Schema 库(mcp.ts:1206)。
  • McpServer 自动装处理器只服务它自己注册的原语;直接用底层 Server 类的人要自己装每个声明能力的处理器(server.ts:78 的注释)。
  • 补全(completion)能力是按需启用的:只有当某个 prompt 参数或资源模板变量带了 completable(...) 补全器时,才会装 completion/complete 处理器(mcp.ts:718mcp.ts:788)。

7. 横向对比

同 shelf 的协议实现里,这种「装饰器/注册式高层 API + 可下钻的底层类」是常见取舍:把简单场景做到极简(registerTool 三行),又留 server.server(底层 Server)给高级用户发自定义通知、装自定义处理器(mcp.ts:70)。

8. 代码地图

主题文件符号
高层服务器类packages/server/src/server/mcp.tsMcpServer
工具处理器装载packages/server/src/server/mcp.tssetToolRequestHandlerscreateToolError
输入/输出校验packages/server/src/server/mcp.tsvalidateToolInputvalidateToolOutput
资源注册/读取packages/server/src/server/mcp.tsregisterResourceResourceTemplate
提示词处理器packages/server/src/server/mcp.tssetPromptRequestHandlerscreatePromptHandler
底层服务器packages/server/src/server/server.tsServerprojectCallToolResult
Standard Schema 校验packages/core-internal/src/util/standardSchema.tsvalidateStandardSchema