MCP 服务加了一行日志就连不上了?stdout 是协议通道,不能往里打字
自己写 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 的八个级别(
debug到emergency) - 客户端通过请求
_meta里的io.modelcontextprotocol/logLevel字段,按请求选择级别 - 服务端不得对没带这个字段的请求发送
notifications/message
最后这条是个硬性约束,容易踩:没带 logLevel 的请求,你就不能发协议日志给它。
考虑到已弃用,新写的服务建议直接走 stderr(stdio)或者自己的日志聚合(HTTP),不要在这个机制上投入。
六、该记录什么
官方列了几类值得记录的重要事件:
- 启动步骤
- 资源访问
- 工具执行
- 错误情况
- 性能指标
其中启动步骤对排查最有价值。MCP 服务最常见的故障就是「起不来」或者「起来就退」——如果你在启动的每个关键步骤都留了记录,日志里能直接看出它走到哪一步挂的。
官方给的日志实践还包括:
- 结构化日志:格式一致、带上下文、加时间戳、跟踪请求 ID
- 错误处理:记录堆栈、包含错误上下文、跟踪错误模式、监控恢复情况
- 性能跟踪:记录操作耗时、监控资源使用、跟踪消息大小、测量延迟
七、安全提醒
官方在调试的安全考量里写了几条,值得单独拎出来,因为日志泄密是很实际的风险:
- 清洗日志(sanitize logs)
- 保护凭证
- 屏蔽个人信息
MCP 服务经常要处理 API key、访问令牌、用户数据。如果你为了排查方便,把完整的请求体打进日志,那些东西就都在日志文件里了——而日志文件可能被打包发给别人、贴进 issue、或者被别的进程读到。
排查时临时打详细日志可以,但记得排查完关掉。
八、排查这个坑的方法
怀疑自己撞上 stdout 污染时:
- 回想最近加了什么。 加日志、加依赖、开调试模式,都可能引入 stdout 输出
- 手动跑一次服务端,看它往 stdout 打了什么。 正常情况下 stdout 里应该只有协议消息(JSON 格式)。如果你看到人类可读的文字,那就是它
- 检查依赖库的默认行为。 尤其是那些会打欢迎信息、版本信息、警告的库
- 确认日志库的默认输出目标。 是 stdout 还是 stderr
- 用 MCP Inspector 单独测,脱开客户端看协议流
第 2 步最直接:跑一下,看 stdout 里有没有不是 JSON 的东西。
九、总结
- stdio 传输下,stdout 是协议通道。往里打日志会破坏协议通信,这是官方明确警告的。
- 报错完全不指向真因——你只会看到连接关闭或进程退出。
- 正确做法是日志走 stderr,stdio 下宿主会自动捕获,日志照样看得到。
- 注意不是你写的 stdout 输出:依赖库的欢迎信息、框架默认配置、调试模式都可能是元凶。
- Streamable HTTP 传输下 stderr 不会被捕获,要自己做日志聚合或用 curl / DevTools 检查。
- 协议内日志
notifications/message自2026-07-28起已弃用,新服务别投入。 - 日志里别留凭证和个人信息——官方在安全考量里明确提了清洗日志和保护凭证。
本文所引官方内容来自 Model Context Protocol 官方调试文档,核对日 2026-08-08。协议版本与弃用状态会随规范演进,以官方规范为准。