MCP 服务加了一行日志就连不上了?stdout 是协议通道,不能往里打字

2026-08-08

自己写 MCP 服务的人,很容易撞上这么一件怪事:

昨天还好好的,加了一行调试输出,今天就连不上了。

而报错完全不提你加的那行——它只说连接关闭了、或者进程意外退出了。你回头检查配置、检查依赖、检查客户端版本,全都没问题。

官方调试文档里有一条明确的警告,正好说的就是这件事。

一、官方的警告

原文的意思是:本地 MCP 服务不应该往 stdout(标准输出)写日志消息,因为这会干扰协议的运行。

这就是全部答案。

二、为什么会这样

stdio 传输的工作方式是:客户端拉起服务端进程,然后通过标准输入和标准输出跟它说协议

  • 客户端往服务端的 stdin 写请求
  • 服务端往自己的 stdout 写响应

stdout 是协议本身在用的通道。

所以你在代码里写一句 print("正在处理请求"),对客户端来说,那就是一段混在协议流里的、不合法的消息。解析当场就崩了。

这个坑的恶劣之处在于三点:

第一,报错完全不指向真因。 客户端只知道「收到了看不懂的东西」或者「连接断了」,它没法告诉你「有人往通道里塞了中文」。

第二,它跟你的直觉相反。 打日志是排查问题最基本的动作。你为了排查问题加了日志,结果日志本身制造了新问题——而且你还会以为是自己刚才改的业务逻辑出了错。

第三,它可能是间接的。 不一定是你自己写的 print。可能是:

  • 引入的某个库在初始化时打了欢迎信息
  • 某个依赖在检测到什么情况时打了警告
  • 框架的默认日志配置就是输出到 stdout
  • 调试模式被意外打开了

最后这几种最难查,因为你根本没写那行代码。

三、正确做法:日志走 stderr

官方给的做法很明确:所有打到 stderr(标准错误)的消息,会被宿主应用自动捕获。

所以 stdio 传输下:

  • 协议 → stdout
  • 日志 → stderr

两条通道分开,互不干扰。而且因为宿主会捕获 stderr,你的日志照样看得到——就在客户端的日志文件里(Claude Desktop 的话,macOS 在 ~/Library/Logs/Claude,Windows 在 %APPDATA%\Claude\logs)。

这不是「牺牲日志换取协议正常」,是「把日志放到正确的通道」。 功能一点不少。

具体怎么写取决于你的语言和框架,通用的原则是:检查你的日志库默认输出到哪里。很多语言的默认打印函数是输出到 stdout 的,而标准日志库通常默认输出到 stderr——这个差别正是坑的来源

官方在讲服务端日志时给的示例代码里,Python 侧用的是标准的 logging 模块,而不是 print。这个选择不是随意的。

四、HTTP 传输下不一样

如果你的服务用的是 Streamable HTTP 传输,情况变了。官方明确说明:

Streamable HTTP 传输下,stderr 不会被客户端捕获。

所以那条「日志走 stderr 就能看到」的便利没有了。官方给的替代方案:

  • 自己做服务端侧的日志聚合,或者用 OpenTelemetry
  • 用标准的 HTTP 工具检查请求和 SSE 流——curl、浏览器 DevTools 的 Network 面板

两种传输的调试方式差别很大,这一点在切换传输方式时特别容易被忽略:你在 stdio 下习惯了「日志自动出现在客户端日志里」,换成 HTTP 之后突然什么都看不到了,会以为是服务挂了。

五、协议内日志:已经弃用了

MCP 协议本身也提供过一种日志机制——通过 notifications/message 把日志发给客户端。

官方明确说明:这个机制自协议版本 2026-07-28 起已弃用,在弃用窗口期内仍然可用。

相关的细节,如果你还在用它:

  • 日志级别是 RFC 5424 的八个级别debugemergency
  • 客户端通过请求 _meta 里的 io.modelcontextprotocol/logLevel 字段,按请求选择级别
  • 服务端不得对没带这个字段的请求发送 notifications/message

最后这条是个硬性约束,容易踩:没带 logLevel 的请求,你就不能发协议日志给它。

考虑到已弃用,新写的服务建议直接走 stderr(stdio)或者自己的日志聚合(HTTP),不要在这个机制上投入。

六、该记录什么

官方列了几类值得记录的重要事件:

  • 启动步骤
  • 资源访问
  • 工具执行
  • 错误情况
  • 性能指标

其中启动步骤对排查最有价值。MCP 服务最常见的故障就是「起不来」或者「起来就退」——如果你在启动的每个关键步骤都留了记录,日志里能直接看出它走到哪一步挂的。

官方给的日志实践还包括:

  • 结构化日志:格式一致、带上下文、加时间戳、跟踪请求 ID
  • 错误处理:记录堆栈、包含错误上下文、跟踪错误模式、监控恢复情况
  • 性能跟踪:记录操作耗时、监控资源使用、跟踪消息大小、测量延迟

七、安全提醒

官方在调试的安全考量里写了几条,值得单独拎出来,因为日志泄密是很实际的风险

  • 清洗日志(sanitize logs)
  • 保护凭证
  • 屏蔽个人信息

MCP 服务经常要处理 API key、访问令牌、用户数据。如果你为了排查方便,把完整的请求体打进日志,那些东西就都在日志文件里了——而日志文件可能被打包发给别人、贴进 issue、或者被别的进程读到。

排查时临时打详细日志可以,但记得排查完关掉。

八、排查这个坑的方法

怀疑自己撞上 stdout 污染时:

  1. 回想最近加了什么。 加日志、加依赖、开调试模式,都可能引入 stdout 输出
  2. 手动跑一次服务端,看它往 stdout 打了什么。 正常情况下 stdout 里应该只有协议消息(JSON 格式)。如果你看到人类可读的文字,那就是它
  3. 检查依赖库的默认行为。 尤其是那些会打欢迎信息、版本信息、警告的库
  4. 确认日志库的默认输出目标。 是 stdout 还是 stderr
  5. 用 MCP Inspector 单独测,脱开客户端看协议流

第 2 步最直接:跑一下,看 stdout 里有没有不是 JSON 的东西。

九、总结

  • stdio 传输下,stdout 是协议通道。往里打日志会破坏协议通信,这是官方明确警告的。
  • 报错完全不指向真因——你只会看到连接关闭或进程退出。
  • 正确做法是日志走 stderr,stdio 下宿主会自动捕获,日志照样看得到。
  • 注意不是你写的 stdout 输出:依赖库的欢迎信息、框架默认配置、调试模式都可能是元凶。
  • Streamable HTTP 传输下 stderr 不会被捕获,要自己做日志聚合或用 curl / DevTools 检查。
  • 协议内日志 notifications/message2026-07-28 起已弃用,新服务别投入。
  • 日志里别留凭证和个人信息——官方在安全考量里明确提了清洗日志和保护凭证。

本文所引官方内容来自 Model Context Protocol 官方调试文档,核对日 2026-08-08。协议版本与弃用状态会随规范演进,以官方规范为准。

相关阅读

想系统学会用 AI?报名体系课或加入会员,照着学、照着用。