tools/call 网络往返 + content 封装
Day 54 验证了发现层(tools/list 返回什么、顺序稳不稳)。今天补最后一块协议拼图——执行层 tools/call:成功结果怎么封装(result.content),失败怎么走标准 JSON-RPC 错误码(-32602 INVALID_PARAMS)。这是 B6 协议三件套(list/call/error)的收口,也是把仓库手写的 McpCallError 错误对齐到标准 JSON
阶段: B6 · 真 MCP server (2026-07-28 spec)(Day 51-60) 标签: #mcp #tools-call #json-rpc-error #content-envelope
今日导引(由浅入深)
Day 54 验证了发现层(tools/list 返回什么、顺序稳不稳)。今天补最后一块协议拼图——执行层 tools/call:成功结果怎么封装(result.content),失败怎么走标准 JSON-RPC 错误码(-32602 INVALID_PARAMS)。这是 B6 协议三件套(list/call/error)的收口,也是把仓库手写的 McpCallError 错误对齐到标准 JSON-RPC 码、跨真实网络往返验证一遍的日子。从今天起,「一个工具能被远端调用、参数非法会被正确拒绝、成功结果结构化返回」三条都在真 transport 上成立。今日最小可判定产出:一段 transcript,2 次 call——1 次失败 -32602 + 1 次成功返回 content。
0. 协议背景速记
- 承上:Day 54 验证了发现层(
tools/list数量/名称/顺序);今天验证执行层。 - 两条路径:成功 →
result.content结构化数组;失败(参数非法)→ JSON-RPC-32602。 - 判定点:transcript,2 次 call——1 次失败
-32602+ 1 次成功返回content。
1. 机理精读
成功路径:结果包在 result.content:
tools/call成功时,工具的输出不是裸返回,而是包在result.content里——一个结构化数组:[{type:'text', text:...}]、[{type:'image', ...}]、[{type:'resource', ...}]等。- 为什么不直接返回裸值?因为 MCP 工具的消费者是 LLM,LLM 需要类型化的内容块才能正确把工具输出拼回上下文(文本块当文本读、图片块走多模态、resource 块按引用处理)。
content数组就是「工具输出 → LLM 可消费内容」的标准封套。
失败路径:参数非法走 -32602 INVALID_PARAMS:
- 当
tools/call的参数过不了 schema 校验(比如缺caseId),server 不能回一个 HTTP 200 +result里塞个「错误对象」。 - 必须走 JSON-RPC error 通道,返回标准错误码
-32602(INVALID_PARAMS)。这是 JSON-RPC 2.0 的硬规定。 - 客户端区分「
result= 成功内容」和「error= 调用失败」靠的就是响应体里是result字段还是error字段。 - 把校验错误塞进 200 的
result里,客户端会误以为调用成功、把错误对象当工具输出喂给 LLM——这是隐蔽且危险的反模式。
为什么要对齐错误码:
- 仓库的
toolRegistry.ts用自定义的McpCallError类承载错误,但它的code字段已经取的是标准 JSON-RPC 值(RPC.INVALID_PARAMS = -32602,line 86)。 - 今天的工作是确保网络版在真 transport 上,参数非法时同样回
-32602——即把「内部抛McpCallError(INVALID_PARAMS)→ JSON-RPCerror.code = -32602」这条映射在网络边界上验证一遍。 - seed 说「补一层 error→
-32602映射」,对照代码现状是:常量已对齐,需补的是「确保 SDK transport 边界仍输出该码」的那层对接/验证。
与相邻概念的边界:业务错误 vs 协议错误要分清——
- schema 校验失败(缺字段、类型错)属于协议层错误,必须走
-32602。 - 工具内部业务逻辑的「软失败」(如「该 case 不满足任何 typology」)通常不是错误——那是合法的成功结果,应当包在
result.content里如实返回,让 LLM/HITL 去判断。 - 把业务软失败误升为 JSON-RPC error,会让客户端把正常结果当调用崩溃处理。
- 反过来,把真正的协议错误(参数缺失)塞进 200
result,又会让客户端误以为成功——两个方向都要避免。
2. 推导 / 手算 / 代码走读
对照基线(Read src/agent/mcp/toolRegistry.ts,真实已存在):
- 错误码常量
RPC(line 85-91):INVALID_PARAMS: -32602、INTERNAL_ERROR: -32603、METHOD_NOT_FOUND: -32601、TOOL_NOT_FOUND: -32001——已是标准 JSON-RPC 码,无需重新发明。 call(name, args)(line 183):先validate(t.spec.inputSchema, args),若errs.length > 0则throw new McpCallError(RPC.INVALID_PARAMS, ...)(line 187)。这就是「参数非法 → -32602」的进程内源头。handle(req)的tools/call分支(line 204-216)逐步走读:- line 205-206:取
req.params?.name,缺 name 直接回error.code = INVALID_PARAMS(-32602)。 - line 208:
const result = this.call(name, req.params?.arguments ?? {})——成功路径。 - line 209:
return { ...base, result: { content: result } }——成功结果包进result.content,正是今天讲的成功封套。 - line 211-213:捕获
McpCallError,折成{ ...base, error: { code: e.code, message: e.message, data: e.data } }——把-32602等码原样搬进 JSON-RPCerror通道。 - line 214:非
McpCallError的兜底,回INTERNAL_ERROR(-32603)。
- line 205-206:取
两次 call 的预期 transcript(手推),以 Day 53 注册的 assessTypology(其 schema 要求 caseId)为例:
- 失败调用(缺
caseId): 请求{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"assessTypology","arguments":{}}}→validate()报$.caseId: required property missing→throw McpCallError(-32602)→handle折成{"jsonrpc":"2.0","id":2,"error":{"code":-32602,"message":"invalid params for assessTypology","data":[...]}}断言:error.code === -32602。 - 成功调用(合法
caseId): 请求{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"assessTypology","arguments":{"caseId":"..."}}}→ 校验通过 → handler 委托src/aml/typology.ts的assessCase/assessCaseV2(line 321/598)算评估 → 包进{"jsonrpc":"2.0","id":3,"result":{"content":[{"type":"text","text":"..."}]}}断言:拿到result.content(非error)。
注:网络版用官方 SDK 时,result.content 的内容块结构由 SDK/handler 返回值决定;进程内 mock 的 handle() 直接把 call() 返回值放进 content(line 209)。两者「成功走 content、失败走 error」的契约一致。
JSON-RPC 保留错误码速查(今天用到的与相邻的):
| 码 | 名称 | MCP 场景 |
|---|---|---|
-32600 | INVALID_REQUEST | 信封本身非法 |
-32601 | METHOD_NOT_FOUND | 未知 method(如拼错 tools/cal) |
-32602 | INVALID_PARAMS | 今天主角:参数过不了 schema |
-32603 | INTERNAL_ERROR | handler 内部抛错(业务崩溃) |
-32001 | (MCP 约定) TOOL_NOT_FOUND | 工具名不存在 |
toolRegistry.ts 的 RPC 常量(line 85-91)覆盖了 -32601/-32602/-32603/-32001——和上表一致,今天只需确认这套码经真 transport 往返后不被吞掉或改写。
为什么校验错误用 -32602 而非 -32603:参数非法是「调用方的错」(传错了),应归 INVALID_PARAMS;handler 内部逻辑崩溃才是「被调方的错」,归 INTERNAL_ERROR。错误码语义对客户端的重试/告警策略有直接影响——-32602 不该重试(参数本身错),-32603 可能值得重试(瞬时故障)。
3. 今日实战
- 用 MCP Inspector 对
assessTypology先传非法参数(缺caseId),断言收到-32602。 - 再传合法参数,断言拿到
result.content。 - 对照
toolRegistry.ts里的错误抛出点(call()line 187 抛McpCallError(INVALID_PARAMS)),在网络版补/验证一层 error→-32602映射,确保跨 transport 仍输出该码。 - 记 transcript:2 次 call(1 次失败
-32602+ 1 次成功返回 content)。
4. 今日实测 / 产出
- 待建——transcript:2 次 call(1 次失败
-32602+ 1 次成功返回 content)。 - 本地可验,无需 key。
5. 常见误区 / 陷阱
- 把业务/校验错误塞进 200 的
result里:校验类错误必须走 JSON-RPC error 通道(-32602)才能被客户端正确处理;塞进result会被当成成功内容喂给 LLM。 - 裸返回工具结果:成功必须包进
result.content结构化数组,不能直接回标量/对象。 - 把业务软失败误升为 error:「不满足任何 typology」是合法成功结果,应进
content,不是-32602。 - 以为常量没对齐就重写错误码:
toolRegistry.ts的RPC常量已是标准码,今天只需在网络边界验证映射成立,别重新发明一套码。
6. 学习资源(每条带 YYYY-MM)
- MCP Specification — tools 章节(
tools/call/result.content结构 / 错误语义,草案 2026-07-28 预定定稿) - JSON-RPC 2.0 Specification(错误对象 /
-32602INVALID_PARAMS 等保留码,2010 起标准,长期有效) src/agent/mcp/toolRegistry.ts的RPC常量 +call()/handle()(2026-06)——错误码对齐与 content 封装对照基线src/aml/typology.ts的assessCase/assessCaseV2(2026-06)——成功路径委托的真实业务实现- MCP Inspector(
npx @modelcontextprotocol/inspector,随 spec 升级用最新版)——tools/call失败/成功路径的手动验证工具
7. 一句话回顾
- 执行层两条路径:成功 →
result.content(结构化内容块数组,喂给 LLM);失败(参数非法)→ JSON-RPC-32602。 - 错误码已对齐:
toolRegistry.ts的RPC.INVALID_PARAMS = -32602等是标准码,今天只需在网络边界验证映射成立。 - 关键纪律:协议错误走 error 通道、业务软失败走
result.content,两个方向都不能混。 - 协议三件套收口:list(Day 54)/ call(今天成功路径)/ error(今天失败路径)齐全,B6 协议骨架完成,明天起填业务工具。
- 诚实状态:2 次 call 的 transcript 为「待建」,本地可验、无需 key。
8. 在 B1→B18 能力曲线上的位置
- B6 协议收口:Day 51 规范 → Day 52 transport → Day 53 Zod 声明 → Day 54 发现层 → 今天执行+错误层。至此 MCP 协议骨架(list/call/error)全部走通。
- 承接 B3(agent loop):B3 的 agent 在进程内调用工具拿结果;今天把「调用→结果封装」这一步抬到 MCP 网络往返,结果封进
result.content后才能被远端 LLM 客户端消费(B6 后半段 Day 57 起会让真模型当 MCP 客户端)。 - 通向 Day 56 + B7:协议骨架就绪,明天填 AML 业务工具(
assessCase/draftSar/listTypologies);下一批 B7 给这套往返加 OAuth 2.1 鉴权与安全闸门。 - 一句话:今天把「工具执行的成功/失败如何跨网络正确返回」钉死,是 MCP server 从「能起」到「能用」的分水岭。
9. 给 hiring manager 的一句话价值
- 「我能讲清 MCP 工具调用里『协议错误 vs 业务软失败』的边界:参数非法走 JSON-RPC
-32602(不该重试)、handler 崩溃走-32603(可重试)、业务软失败走result.content(正常结果)——这套区分直接决定客户端的重试/告警策略是否正确。」
SOTA检查 (2026-06 更新)
- 当前主流:
tools/call成功走result.content结构化数组、失败走标准 JSON-RPC error 码,仍是 SOTA 契约。 - 是否仍 SOTA:content 结构在 spec 中含
text/image/resource等;以 07-28 定稿字段为准。 - 过时黑名单(AVOID):把业务错误塞进 200 的
result里——校验类错误必须走 JSON-RPC error 通道才能被客户端正确处理。 - 下次复查点:07-28 规范定稿后复验
content内容块类型清单与tools/call错误码约定;Inspector 当周用最新版。
衔接
- 昨天:Day 54 — tools/list 网络语义(发现层契约:数量/名称/顺序稳定)
- 今天:tools/call 执行层收口——成功
result.content+ 失败-32602,协议三件套(list/call/error)齐全 - 明天:Day 56 — 迁移 AML 工具集上网(把
assessCase/draftSar/listTypologies三个 AML 能力按契约封成 MCP 工具,Inspector 逐个调通)