MCP把工具失败写入结构化错误结果的实现方法
来源:17golang原创
时间:2026-09-19 22:06:52 208浏览 收藏
MCP 工具调用失败时,最稳的处理方式不是把一段“失败了”的字符串当成正常返回,而是在工具结果中明确设置 isError: true,同时让 content 告诉模型发生了什么。只有未知工具、无效协议参数或服务端无法完成请求这类协议层问题,才应该走 JSON-RPC 的 error。这样客户端知道该重试工具,模型也不会把业务失败误认为成功数据。
官方规范:https://modelcontextprotocol.io/specification/2025-06-18/server/tools
- 可由模型修正的参数、外部 API 或业务规则失败,放进工具结果并设置
isError: true。 content面向模型和用户,structuredContent面向程序;失败时不要让客户端继续信任成功数据。- 未知工具和协议级无效请求属于 JSON-RPC 错误,不能用业务错误结果掩盖。
先按失败层级选择返回方式
我在接入工具时最容易踩的坑,是把所有异常都直接抛到 transport 层。这样做看起来省事,但库存不足、第三方限流、权限不够等情况,本来都可以由模型修正参数或换方案,却被客户端当成一次请求崩溃。MCP 的边界很清楚:工具已经被正确找到并开始执行,执行结果里用 isError: true;请求本身无法按协议处理,才返回 JSON-RPC error。
| 场景 | 返回形态 | 调用方动作 |
|---|---|---|
| 业务条件不满足 | result.isError=true | 读取提示,修正条件或换工具 |
| 外部 API 暂时失败 | 工具结果中的错误文本 | 按错误码决定重试,避免盲重试 |
| 未知工具、非法 JSON-RPC 参数 | 顶层 error | 修复协议请求或工具注册 |
用 content 和 structuredContent 组成错误结果
成功结果可以同时提供可读文本和结构化对象:前者便于模型理解,后者便于客户端渲染或记录。失败时要先看 isError,再决定是否读取结构化字段。错误消息应包含动作、可修正方向和稳定错误码,不要泄露 SQL、内部路径或第三方响应中的密钥。

type ToolResult = {
content: Array;
isError?: boolean;
structuredContent?: { code: string; retryable: boolean; message: string };
};
function failed(code: string, message: string, retryable: boolean): ToolResult {
// content 给模型可读的下一步,结构化字段给客户端稳定消费。
return {
content: [{ type: "text", text: `${code}: ${message}` }],
isError: true,
structuredContent: { code, retryable, message },
};
}
function succeeded(data: { id: string; status: string }): ToolResult {
// 成功分支保持相同的数据语义,避免调用方按文本猜字段。
return {
content: [{ type: "text", text: `任务 ${data.id} 当前状态为 ${data.status}` }],
structuredContent: data,
};
如果当前 SDK 或客户端对失败结果不承诺读取 structuredContent,可以把关键错误码和修正建议放在文本 content 中;不要依赖只有某个客户端才解析的扩展字段。成功结果使用 outputSchema 时,结构化对象还应符合该 schema。
在工具边界捕获可预期业务异常
工具实现应把“用户能修正”的异常和“开发者必须排查”的异常分开。下面的 TypeScript 示例只把已知业务错误转换成 MCP 工具错误;未知异常记录服务端日志,向调用方返回不泄露内部细节的通用信息。
server.registerTool("reserve-seat", { inputSchema }, async ({ seatId }) => {
try {
const seat = await seats.get(seatId);
if (!seat) {
// 不存在是可修正的业务条件,模型可以改用其他座位号。
return failed("SEAT_NOT_FOUND", "座位不存在,请重新选择座位号", false);
}
if (seat.status !== "available") {
// 已被占用同样是工具执行结果,不应伪装成协议崩溃。
return failed("SEAT_UNAVAILABLE", "座位已被占用,请选择其他座位", true);
}
await seats.reserve(seatId);
return succeeded({ id: seatId, status: "reserved" });
} catch (error) {
// 真实项目应记录 traceId 和完整异常,但不把内部堆栈交给模型。
logger.error({ error, seatId }, "reserve-seat failed");
return failed("DEPENDENCY_UNAVAILABLE", "座位服务暂时不可用,请稍后重试", true);
}
});
这里的 retryable 是业务约定,不是 MCP 协议字段,作用是让宿主决定是否自动重试。模型看到 SEAT_NOT_FOUND 时应改变参数,看到 DEPENDENCY_UNAVAILABLE 时才考虑延迟重试。若输入根本无法通过工具 schema 校验,或调用了未注册工具,就不要在业务函数里制造一个假的工具结果。

客户端先检查 isError 再读取数据
客户端不要只判断请求有没有抛异常。一次 tools/call 可能正常收到结果,但结果里的 isError 已经说明工具没有完成目标。建议把这一步封装成统一适配器,并把错误码、是否可重试和原始文本写入审计日志。
const result = await client.callTool({ name: "reserve-seat", arguments: { seatId } });
if (result.isError) {
// 失败结果仍是协议响应,先展示可行动信息,再决定是否重试。
const text = result.content
.filter((item) => item.type === "text")
.map((item) => item.text)
.join("\n");
return { ok: false, message: text, retryable: false };
}
// 只有确认成功后,才把 structuredContent 当作业务数据使用。
return { ok: true, data: result.structuredContent };
发布前至少覆盖三组用例:有效座位返回成功对象;已占用座位返回 isError=true 且保留业务错误码;未知工具或非法参数返回协议错误。检查日志时还要确认 traceId 能把一次工具调用、外部依赖和最终结果串起来。
常见问题
工具返回错误字符串但不设置 isError 可以吗?
不建议。客户端会把它当成成功文本,模型也可能继续使用错误内容;可修正失败应明确设置 isError: true。
业务错误应该直接抛 JSON-RPC error 吗?
只有请求无法按 MCP 协议处理时才适合。库存不足、外部限流和权限条件通常属于工具执行错误,应留在结果中。
失败结果还要返回 structuredContent 吗?
可以,但要先确认客户端约定;关键错误码和修正建议必须同时出现在 content,避免只依赖结构化扩展。
-
278 收藏
-
483 收藏
-
291 收藏
-
195 收藏
-
412 收藏
-
398 收藏
-
359 收藏
-
171 收藏
-
234 收藏
-
380 收藏
-
191 收藏
-
272 收藏
-
251 收藏
-
357 收藏
-
473 收藏
-
381 收藏
-
195 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习