mcp-typescript-sdk — 本课题摘录
读了哪几篇: 01-high-level-server(高层服务器 API)。
其余三篇(协议核心与传输、双纪元编解码、客户端与版本协商)本轮没读——属「接外部能力」课题。
这一家在本课题里的位置:它是"工具在服务端这一侧长什么样"的 TypeScript 实现参照,跟我们同语言。
它对本课题回答了什么
决定三:一次工具调用的五步流水线
请求到达
│
① 查注册表 ──未找到/已禁用──► 抛协议错误
│ 找到
② 校验入参 ──失败──► 抛协议错误
│ 通过(拿到已转型的参数)
③ 执行你的函数
│
④ 校验出参 ──声明了返回格式却没给结构化内容──► 抛协议错误
│ 通过
⑤ 投影返回(把中性结果翻译成当前协议版本的形状)
(依据:协议库 · MCP TypeScript SDK · 高层服务器 API(McpServer) —— tools/call 的五步是查注册表、validateToolInput、executeToolHandler、validateToolOutput、projectCallToolResult)
注意第 ② 步的产出:处理器拿到的是"已校验、已转型"的参数,不是原始 JSON。 这一步把"模型给的东西可能是任何形状"这个问题一次性挡 在了业务代码之外。
决定三最重要的一条:业务失败与协议错误要分开
这是这一篇最值得抄的一条区分。
| 什么情况 | 怎么回 | 后果 |
|---|---|---|
| 工具业务失败(你的函数抛了) | 包成一个带"这是错误"标记的正常结果 | 模型能看到错误文本并自我纠正 |
| 协议级问题(工具不存在、参数非法) | 抛协议错误 | 调用方知道这是接口用错了 |
(依据: shelf=protocol/mcp-typescript-sdk#01-high-level-server @3924de99df83 事实=工具业务失败被 catch 后包成 {content, isError:true} 的正常结果而非 JSON-RPC 协议错误,只有工具不存在/参数非法这类协议级问题才抛 ProtocolError;这让 LLM 能看到错误文本并自我纠正)
这条把"错误即消息"这条通则说清楚了边界**:** 不是所有错都变消息——只有"工具跑的时候出了事"才变消息;"你根本不该这么调"是协议错误。 跟 langchain4j 的"参数错快速失败、执行错回喂模型"是同一刀,切的位置也一样。
决定二:参数格式不绑死某一个校验库
新版不再硬依赖某个特定的校验库,而是接受任何实现了一份通用规范的库。校验走统一入口。 (依据:协议库 · MCP TypeScript SDK · 高层服务器 API(McpServer) —— v2 不再硬依赖 Zod,inputSchema 接受任何实现 Standard Schema 的库(Zod v4 / Valibot / ArkType),校验走 validateStandardSchema 统一入口)
对我们有直接参考: 定义工具时"参数怎么描述"这件事,应该依赖一个抽象契约而不是某个具体库——否则换库要动所有工具。
它保留了一个已标记废弃的旧写法(直接传一个字段字典,自动包成对象),纯粹为了兼容老代码。
一个极容易踩的细节:判断"有没有结构化内容"必须用"是不是未定义"
不能用真值判断。 因为结构化内容合法地可以是 null、0、false、空字符串——这些都是有效的 JSON 值。
用真值判断就会把合法的 0 当成"缺失"。
(依据:协议库 · MCP TypeScript SDK · 高层服务器 API(McpServer) —— 判断有没有 structuredContent 必须用 === undefined 而非真值判断,因为它合法地可以是 null/0/false/"" 这些有效 JSON 值,用 falsy 判断会把合法的 0 当成缺失)
这条小到像是代码洁癖,但它会造成"工具返回 0 时静默报错"这种极难查的 bug。 我们写工具结果处理时会遇到同一个坑。
一处"多轮没走完"的特判
工具可以返回一个"我还需要更多输入"的结果。这种结果在出参校验之前就被拦截并原样透传——因为它不是最终输出,不该跑返回格式校验。 (依据:协议库 · MCP TypeScript SDK · 高层服务器 API(McpServer) —— isInputRequiredResult 在输出校验之前就拦截并原样透传,因为它不是最终输出、不该跑 outputSchema 校验)
这就是 mcp-spec 那条"一轮没走完的中间态"在实现侧的落点。中间态要在校验之前就分流,否则会被当成畸形的最终结果。
它的做法(可以抄的部分)
注册表 + 懒加载处理器: 第一次注册某 类东西时才装对应的处理器,用一个标志位防重复装。
但有一个规范坑已经被处理: 如果构造时预先声明了某项能力,处理器会立即装上,哪怕还没注册任何东西。理由是规范要求"声明了能力就必须响应它的清单方法",否则会回"方法不存在",违反规范。 (依据:协议库 · MCP TypeScript SDK · 高层服务器 API(McpServer) —— 预先声明 tools 能力时构造函数立即装处理器(哪怕还没注册任何工具),因为规范要求「声明了能力就必须响应其 list 方法」,否则回 -32601 违反规范)
泛化:声明了什么就要能响应什么,哪怕是空的。 这是接口契约的基本要求,但很容易被"还没东西就先不装"的懒加载优化破坏。
格式转换失败只警告、不抛。 理由是:对着"会忽略该字段的客户端"本地开发时不该被阻塞。
这是一个"开发体验优先"的取舍,值得记住它的适用条件:这个字段不是必需的时候才能这么做。
转换结果被缓存。 因为每个请求一个工厂的模型下,同一个工具可能被反复转换。
中性结果与版本投影分离: 处理器本身跟协议版本无关,只产出中性结果;版本差异由一个专门的投影层处理。处理器永远不碰这些差异。
它没回答什么
- 循环怎么写——它是 服务端一侧,不是 agent 一侧。
- 模型输出怎么解析成调用——不在这里。
- 停止条件——不在它的范围。
坑与代价
- 高层 API 只服务它自己注册的东西。 直接用底层类的人要自己装每个声明能力的处理器。
- 那个已废弃的裸字典写法只对某一个特定库有效。 兼容层往往有这种"只在旧路径上成立"的限制。
- 它是服务端视角。 我们的最小循环是"客户端 + 本地函数",不需要这一整套。
判断(无锚): 但"五步流水线"和"业务失败 vs 协议错误"两条应该现在就用——即使工具就是本地函数,这两条也成立。 如果错,会错在: 如果第一版工具全都不会失败(纯读、纯计算),那"业务失败"这一支暂时是空的,区分没有意义;等第一个会失败的工具出现再补也不迟。