[v2] JSON-RPC error responses leave client OpenTelemetry spans UNSET

当使用 MCP Python SDK v2 的 JSONRPCDispatcher 处理返回 JSON-RPC 错误(如 -32602 )的请求时,客户端生成 OpenTelemetry span 虽然请求本身已正确抛出 MCPError ,但 span 状态仍停留在 UNSET ,缺失 error

快速结论:当使用 MCP Python SDK v2 的 JSONRPCDispatcher 处理返回 JSON-RPC 错误(如 -32602)的请求时,客户端生成 OpenTelemetry span 虽然请求本身已正确抛出 MCPError,但 span 状态仍停留在 UNSET,缺失 error.type、rpc.response.status_code 和异常事件,导致错误链路在监控中不可见。

适用环境:已在 modelcontextprotocol/python-sdk 的 main 分支提交 11934c90aeff5e1e68aee223edd00c5d1fce1d5c 上复现,涉及 v2 的 JSONRPCDispatcher、内存中运行的 MCP Server(lowlevel.server.Server)与 Client(mode="legacy"),并使用 OpenTelemetry SDK 的 InMemorySpanExporter 收集 span。Issue 中未声明具体操作系统、Python 或 CUDA 版本。

最快修复方案:暂无确认的一步修复方案。Issue 讨论中提出的方向是:在 send_raw_request() 中,于 span 上下文退出前检测 ErrorData,并在 span 上写入 error.type、rpc.response.status_code 以及 StatusCode.ERROR,与 server/_otel.py 的服务端做法保持一致。

注意事项:该方案在 Issue 关闭时仍处于提议阶段,尚无合并的补丁可供直接升级;不要将其视为已验证的官方修复。此外,问题的范围仅限 JSON-RPC/stream-backed dispatcher 调用,不涉及直接的面内 dispatch,也未涉及实际部署的传输层、收集器或生产频率。

问题场景

你在使用 MCP Python SDK v2,并打开了客户端 OpenTelemetry 埋点。当 MCP Server 端对某个资源类请求(如 resources/list)返回 JSON-RPC 错误响应时,客户端的 Client / session.list_resources() 调用会正确抛出 MCPError,但通过 InMemorySpanExporter 或任意 OTel 后端观察到的 CLIENT span 仍然是一个正常的退出状态,无法与成功的控制请求区分。这通常发生在排查可观测性、审计合规或错误告警覆盖时被发现。

报错原文

[v2] JSON-RPC error responses leave client OpenTelemetry spans UNSET

METHOD=resources/list CONTROL=UNSET RED_CODE=-32602 RED_MESSAGE='forced failure' RED_STATUS=UNSET ERROR_TYPE=None RPC_STATUS=None EVENTS=0

复现时每次运行都产生上述输出:成功对照 span 与错误 span 的状态都是 UNSET,错误 span 没有 error.type、没有 rpc.response.status_code,也没有异常事件。

原因分析

根据 Issue 正文以及后续讨论确认的根因:在 send_raw_request()(src/mcp/shared/jsonrpc_dispatcher.py,提交 11934c90 的第 368–434 行)中,客户端 span 在收到 outcome 之后先退出上下文,然后才在第 432–433 行把 ErrorData 转换为 MCPError。span 的上下文管理器因此观察不到任何异常,也就不会把状态设置为 ERROR,也不会附加语义约定属性。

这与服务端中间件 server/_otel.py 的现有做法不一致,后者会记录错误属性和状态;也与 #2381 中“被传播的 MCPError 应由客户端 span 记录”的设计假设相矛盾。

环境排查

  • 确认 MCP Python SDK 是否为当前 main 分支(Issue 报告基于 11934c90aeff5e1e68aee223edd00c5d1fce1d5c),v1 的 BaseSession 架构不在此问题范围。
  • 确认使用的是 v2 的 JSONRPCDispatcher,且为 JSON-RPC/stream-backed dispatcher 调用,而不是直接的面内 dispatch。
  • 确认客户端已配置 OpenTelemetry SDK,并使用可导出/可查询的 span exporter(Issue 复现使用 InMemorySpanExporter + SimpleSpanProcessor)。
  • 确认 OTel 版本与 SpanKind.CLIENT 过滤方式,便于正确抓取对应客户端 span。
  • Issue 未声明具体 Python、CUDA、显卡、模型或部署传输环境,排查时无需据此补写版本号。

解决步骤

  1. 先按 Issue 提供的复现思路,在内存中构造一对成功/失败请求(例如 resources/list),用 InMemorySpanExporter 分别导出两次的 CLIENT span,确认失败 span 的 status 为 UNSET、无 error.type 且无异常事件。
  2. 定位 send_raw_request() 内 span 的进入与退出位置,确认 ErrorData 到 MCPError 的转换发生在 span 退出之后。
  3. 可优先尝试的修复:在 span 上下文内收到 outcome 后,若判定为 ErrorData,就在退出前把 error.type、rpc.response.status_code 和 StatusCode.ERROR 写入 span,参照 server/_otel.py:35-60 的服务端模式;另一种等效做法是让 ErrorData 到 MCPError 的转换发生在活跃的客户端 span 内。
  4. 为修复补上内存回归测试,覆盖成功与 JSON-RPC 错误两种响应,确保失败 span 与成功对照 span 可区分。
  5. 将改动范围限制在 v2 的 JSONRPCDispatcher,并运行针对性测试、Ruff 和 Pyright。

验证方法

再次运行同一个内存复现:失败请求仍应抛出 MCPError;对应的 CLIENT span 状态应为 ERROR,包含 error.type,并携带 JSON-RPC 错误码(复现中为 -32602)/ rpc.response.status_code,与成功对照 span 明显区分。这可作为该 Issue 的验收标准。

参考来源

modelcontextprotocol/python-sdk #3174

GamsGo AI

AI 工具推荐

想把多个 AI 模型放在一个入口?

GamsGo AI 集成 ChatGPT、DeepSeek、Gemini、Claude、Midjourney、Veo 等常用模型,适合写作、绘图、视频和日常 AI 工作流。

了解 GamsGo AI

推广链接:通过此链接购买,我可能获得佣金,不影响你的价格。

这个方案解决了吗?

celebrityanime
celebrityanime
文章: 27144

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注