我的 MCP 服务器以前经常莫名其妙地停止工作。没有崩溃转储,日志里也没有堆栈跟踪。客户端连接时一切正常,但几小时后,整个系统就陷入了沉默。请求消失了,另一端的 AI agent 除了面对一片空白外,什么也接收不到。
这在 Model Context Protocol (MCP) 生态系统中是一个令人沮丧且常见的问题。该协议定义了 AI agent 如何发现并调用外部工具,但规范假设你会自行处理错误。大多数教程和入门实现都会跳过这部分。它们专注于“快乐路径”(happy path):标注函数、通过服务器公开它,然后返回一个干净的结果。它们很少向你展示当外部 API 出现网络波动,或者当模型幻觉出一个参数名称并发送垃圾输入时会发生什么。结果就是一个脆弱的服务器,看起来运行正常,但实际上已经“死”了好几个小时。
为什么空白响应比崩溃更糟糕
当一个未处理的异常在 MCP 工具处理器中溜掉时,传输层通常会将其吞掉。服务器进程保持存活,套接字(socket)保持开启,但客户端收到的却是空响应。这比显眼的崩溃更危险,因为你的监控系统可能察觉不到。进程仍在运行,端口仍在监听,然而每一次工具调用都返回空值。
AI 模型不会将沉默解释为失败。它会将沉默解释为一次成功调用但未产生数据。这种空白响应会训练模型进行“即兴发挥”。它开始幻觉事实来填补空白,或者陷入重复尝试同一个错误调用的循环中。像瞬时网络超时或无效工具参数这样的小问题,绝不应该导致这种行为。
包装器模式:三道防线
我通过将每个工具处理器(tool handler)包装在一个薄薄的错误恢复层中解决了这个问题。这个包装器并不试图预测每一种可能的失败,而是对它们进行分类并做出相应响应。
ConnectionError 和 TimeoutError
当你的服务器与外部 API 通信且网络出现波动时,就会出现这些错误。直觉上的修复方法是重启整个 MCP 服务器进程。千万不要这样做。重启会断开活跃的客户端连接,清除任何内存状态,并强制进行完整的重新初始化。相反,你应该捕获连接失败,并仅重新连接你的工具所使用的传输层或 HTTP 客户端。服务器保持“热启动”状态,能够立即准备好处理下一个请求。
ValueError
当 AI 客户端发送格式错误的参数时,就会看到这个错误。也许模型虚构了一个参数,在需要整数的地方传递了字符串,或者遗漏了必填字段。如果你让这个错误在未处理的情况下向上冒泡,客户端要么会崩溃,要么会收到空白回复。在包装器内部捕获它,然后构建一条清晰、具体的错误消息,准确地告诉模型哪里出了问题。说明哪个参数失败了以及预期的值是什么。大多数现代 AI 模型在读到该消息后,会在下一轮对话中进行自我修正。模糊的错误会浪费推理周期,而精确的错误能立即解决问题。
General Exceptions
保留一个安全网。如果错误不属于上述类别,请记录详细日志供你自己查看,并向客户端返回一个干净、通用的失败响应。这可以防止一个奇怪的边缘情况导致所有人的会话都中断。服务器得以存活,客户端收到了失败信号,同时你在日志中保留了足够的上下文以便日后调试。
isError 标志是必须遵守的
这是一个真正决定你的修复是否有效的细节。MCP 响应包含一个 isError 布尔字段。如果发生异常,而你返回了错误消息却没将 isError 设置为 true,客户端会将该错误文本视为成功的工具结果。
想象一下,你的外部 API 触发了频率限制(rate limit)。你捕获了异常并返回字符串 "API rate limit exceeded",但将 isError 保持为 false。客户端会将该字符串传入模型的上下文窗口,仿佛它是真实的工具输出一样。模型随后会尝试对该文本进行推理,仿佛它就是数据。它可能会在总结中引用该错误,或者更糟的是,它可能会在错误文本与其他事实之间产生幻觉,建立虚假联系。你把一个临时的基础设施小故障变成了一个错误信息的来源。
返回错误负载时,务必将 isError 设置为 true。这能向客户端发出明确信号,表明工具调用失败,从而让模型决定是重试、请求澄清,还是尝试完全不同的工具。
明确哪些错误该捕获,哪些该终止
不要用一个会吞掉所有错误的盲目 try-catch 块包裹整个服务器。某些错误意味着服务器应该立即停止。如果启动时缺少必要的环境变量,或者配置文件损坏,任何请求级别的捕获都无济于事。为这类致命错误创建特定的异常类,并让它们直接导致进程崩溃。
规则很简单:如果错误是暂时的或仅限于单个请求,请捕获并恢复。如果错误意味着后续每个请求都注定会失败,那就让服务器“大声地”崩溃。启动时的快速失败,远比一个在损坏状态下勉强维持数日的服务器要好得多。
在需要之前就加入可观测性
一旦部署了包装器(wrapper),请将其与结构化日志结合使用。以 JSON 格式记录每一次工具调用及其结果。记录内容应包括工具名称、原始参数、延迟时间,以及是成功、失败还是重试。
这种规范会迅速带来回报。当你注意到错误激增时,可以通过工具进行过滤,并在几分钟内发现规律。也许某个特定的外部 API 每天都在同一时间开始出现超时,指向了一个你并不知晓的定期维护窗口。也许某个工具持续收到格式错误的参数,从而揭示了上游提示词工程(prompt engineering)的缺陷。埋没在堆栈跟踪中的纯文本日志会让这种侦查工作变得异常痛苦,而结构化的 JSON 则能让这一切变得轻而易举。
生产环境的效果
在过去的三个星期里,我在两个生产环境的 MCP 服务器上运行了这种包装器模式。在这段时间内,我没有遇到过任何“静默失败”。在添加包装器之前,我平均每天都会遇到大约一次无法解释的失败。这种模式并不复杂,但其影响却非常巨大,因为它将可恢复的噪声与真正的问题区分开了。
静默失败的代价比崩溃更高。崩溃会触发你的告警系统,而沉默只会侵蚀信任。今天你的 AI agent 还能返回有用的工具数据,明天它可能就开始胡编乱造,因为服务器早在几小时前就停止响应了。包装器模式填补了这一鸿沟。它让你的服务器在轻微波动中保持运行,为模型提供足够的上下文来纠正自身的错误,并确保当真正致命的问题发生时,你能立即收到通知。
如果你现在正在构建 MCP 工具,请从包装器和 isError 标志开始。其他的一切都只是后续的清理工作。
