package mcp import ( "errors" "fmt" sdkjsonrpc "github.com/modelcontextprotocol/go-sdk/jsonrpc" ) // 本文件是「第三方 MCP server 的错误怎么写进本包错误」的**唯一**收敛点。 // // 为什么需要它:MCP server 是第三方进程/远端服务,它的错误文本是**上游自由文本** —— // 它可以把刚收到的工具参数、读到的文件内容、甚至是它自己的系统提示词塞进 // `error.message`。原样经 SDK error 进本包错误,那条文本就会随 `errors.Is` 分类链 // 一路进日志、进管理面板、进模型可见视图。与 trace.UpstreamError 同一口径: // **默认不带自由文本**,只留可分类的骨架。 // // 留下的骨架(够定位,不够回显): // - 哪台 server / 哪把工具 —— 由调用点的 %w 文案给(名字来自配置与 tools/list, // 不是错误文本); // - JSON-RPC code —— 协议枚举(-32601 method not found、-32602 invalid params …), // 是本文件唯一允许出镜的「对端给的值」,且以数字渲染; // - message 的**字节数** —— 能区分「对端给了句话」与「对端什么都没说」,不带那句话。 // upstreamFailure 是一次第三方 server 失败在本包错误链里的形态。它的 Error() 只 // 渲染 code 与长度;原始原因挂在 cause 上,只供 errors.Is / errors.As 分类。 // // 为什么不直接把 SDK 的 err 用 %w 包上:%w 只加前缀,被包的那条文本仍然在 // err.Error() 里 —— 包一层不是修复,只是换个位置泄漏(同 trace.URLCause 的口径)。 type upstreamFailure struct { // code 是 JSON-RPC 错误码;hasCode 为 false 表示这条错误不是线上响应里的 // 结构化错误(传输层失败、SDK 内部错误、ctx 取消等),此时不带任何码。 code int64 hasCode bool // length 是被扣下的自由文本的字节数。 length int // cause 只用于分类,绝不参与 Error()。 cause error } // Error 渲染一行只含 code / 长度的诊断;自由文本一个字节都不出。 func (f *upstreamFailure) Error() string { if f.hasCode { return fmt.Sprintf("mcp server error: code=%d message_bytes=%d", f.code, f.length) } return fmt.Sprintf("mcp server error: %T message_bytes=%d", f.cause, f.length) } // Unwrap 让 errors.Is/As 仍然看得见原始原因(context.Canceled、 // context.DeadlineExceeded、*jsonrpc.Error 的码…),而 Error() 绝不渲染它。 // 少了它,取消会被上层当成普通工具失败继续跑。 func (f *upstreamFailure) Unwrap() error { return f.cause } // upstreamError 把一次第三方 server 失败收敛成**不带自由文本**的错误。 // // 收 code 用的 errors.As 会遍历整条链:SDK 常把线上错误再包一层(fmt 包装、 // JSON-RPC 信封),所以不能只断言最外层。 func upstreamError(err error) error { if err == nil { return nil } failure := &upstreamFailure{cause: err, length: len(err.Error())} var wire *sdkjsonrpc.Error if errors.As(err, &wire) { failure.code = wire.Code failure.hasCode = true // WireError.Error() 就是 Message 本身,所以这里量的正是被扣下的那段文本。 failure.length = len(wire.Message) } return failure }