返回 AICAP-180
B6 · Day 55真 MCP server (2026-07-28 spec)

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-RPC error.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: -32602INTERNAL_ERROR: -32603METHOD_NOT_FOUND: -32601TOOL_NOT_FOUND: -32001——已是标准 JSON-RPC 码,无需重新发明。
  • call(name, args)(line 183):先 validate(t.spec.inputSchema, args),若 errs.length > 0throw new McpCallError(RPC.INVALID_PARAMS, ...)(line 187)。这就是「参数非法 → -32602」的进程内源头。
  • handle(req)tools/call 分支(line 204-216)逐步走读:
    1. line 205-206:取 req.params?.name,缺 name 直接回 error.code = INVALID_PARAMS(-32602)。
    2. line 208:const result = this.call(name, req.params?.arguments ?? {})——成功路径。
    3. line 209:return { ...base, result: { content: result } }——成功结果包进 result.content,正是今天讲的成功封套。
    4. line 211-213:捕获 McpCallError,折成 { ...base, error: { code: e.code, message: e.message, data: e.data } }——把 -32602 等码原样搬进 JSON-RPC error 通道。
    5. line 214:非 McpCallError 的兜底,回 INTERNAL_ERROR(-32603)。

两次 call 的预期 transcript(手推),以 Day 53 注册的 assessTypology(其 schema 要求 caseId)为例:

  1. 失败调用(缺 caseId): 请求 {"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"assessTypology","arguments":{}}}validate()$.caseId: required property missingthrow McpCallError(-32602)handle 折成 {"jsonrpc":"2.0","id":2,"error":{"code":-32602,"message":"invalid params for assessTypology","data":[...]}} 断言:error.code === -32602
  2. 成功调用(合法 caseId): 请求 {"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"assessTypology","arguments":{"caseId":"..."}}} → 校验通过 → handler 委托 src/aml/typology.tsassessCase/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 场景
-32600INVALID_REQUEST信封本身非法
-32601METHOD_NOT_FOUND未知 method(如拼错 tools/cal
-32602INVALID_PARAMS今天主角:参数过不了 schema
-32603INTERNAL_ERRORhandler 内部抛错(业务崩溃)
-32001(MCP 约定) TOOL_NOT_FOUND工具名不存在

toolRegistry.tsRPC 常量(line 85-91)覆盖了 -32601/-32602/-32603/-32001——和上表一致,今天只需确认这套码经真 transport 往返后不被吞掉或改写。

为什么校验错误用 -32602 而非 -32603:参数非法是「调用方的错」(传错了),应归 INVALID_PARAMS;handler 内部逻辑崩溃才是「被调方的错」,归 INTERNAL_ERROR。错误码语义对客户端的重试/告警策略有直接影响——-32602 不该重试(参数本身错),-32603 可能值得重试(瞬时故障)。

3. 今日实战

  1. 用 MCP Inspector 对 assessTypology 先传非法参数(缺 caseId),断言收到 -32602
  2. 再传合法参数,断言拿到 result.content
  3. 对照 toolRegistry.ts 里的错误抛出点(call() line 187 抛 McpCallError(INVALID_PARAMS)),在网络版补/验证一层 error→-32602 映射,确保跨 transport 仍输出该码。
  4. 记 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.tsRPC 常量已是标准码,今天只需在网络边界验证映射成立,别重新发明一套码。

6. 学习资源(每条带 YYYY-MM)

  • MCP Specification — tools 章节(tools/call / result.content 结构 / 错误语义,草案 2026-07-28 预定定稿)
  • JSON-RPC 2.0 Specification(错误对象 / -32602 INVALID_PARAMS 等保留码,2010 起标准,长期有效)
  • src/agent/mcp/toolRegistry.tsRPC 常量 + call()/handle()(2026-06)——错误码对齐与 content 封装对照基线
  • src/aml/typology.tsassessCase/assessCaseV2(2026-06)——成功路径委托的真实业务实现
  • MCP Inspector(npx @modelcontextprotocol/inspector,随 spec 升级用最新版)——tools/call 失败/成功路径的手动验证工具

7. 一句话回顾

  • 执行层两条路径:成功 → result.content(结构化内容块数组,喂给 LLM);失败(参数非法)→ JSON-RPC -32602
  • 错误码已对齐toolRegistry.tsRPC.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 逐个调通)