MCP Python SDK 日志实践:标准库 logging、log_level 配置与 stdio 进程的 stdout/stderr 分流
【免费下载链接】python-sdkThe official Python SDK for Model Context Protocol servers and clients项目地址: https://gitcode.com/gh_mirrors/pythonsd/python-sdk
本篇技术指南围绕 MCP 官方 Python SDK(mcp包)的日志处理展开:协议层的 logging capability 在 2026-07-28 规范修订中被弃用且无替代,因此 SDK 推荐的唯一模式是使用 Python 标准库logging。读完本篇,你将掌握工具内打日志的标准写法、MCPServer(log_level=...)的底层配置机制、stdio 服务器下 stdout/stderr 的归属规则,以及如何用 MCP Inspector 验证日志不会泄漏到协议链路。
核心结论:用标准库打日志,不要用协议层日志能力
MCP 协议本身曾提供一种日志能力(logging capability):服务器可以通过Context对象上的方法,把日志消息以通知(notification)形式推送给客户端。2026-07-28 版本的规范弃用了该能力且不提供替代方案,因此官方文档(含本仓库的 法语版日志文档 与 英文原版)不再教授它。所有已弃用功能的完整清单及替代做法见 已弃用功能(英文对照:deprecated)。
正确的做法就是在 MCP 服务器里打日志和在任何其他 Python 程序里一样——使用标准库logging。
一个会打日志的工具:完整示例
官方文档给出的完整示例位于 docs_src/logging/tutorial001.py,原文如下,可直接复制运行:
import logging from mcp.server import MCPServer logger = logging.getLogger(__name__) mcp = MCPServer("Bookshop") @mcp.tool() def search_books(query: str) -> str: """Search the catalog by title or author.""" logger.info("Searching for %r", query) return f"Found 3 books matching {query!r}."要点:
logging.getLogger(__name__)给你一个以模块名命名的 logger。在文件顶部创建一次即可,不要在每次调用里重复创建。- 在工具函数内部调用
logger.info(...),与任何普通函数无异——不需要注入、不需要await、没有任何 MCP 专属 API。
调用该工具并检查完整结果(文档原文给出的验证方式):
result.content # [TextContent(text="Found 3 books matching 'dune'.")] result.structured_content # {'result': "Found 3 books matching 'dune'."}注意:日志行不在结果的任何位置。日志是为你(服务器的运维者)打的,模型永远看不到它。如果模型需要读到某段内容,请用return返回它。
这个结论有测试背书:tests/docs_src/test_logging.py 中test_the_log_line_never_reaches_the_client用inline_snapshot精确断言了call_tool返回的CallToolResult只包含content与structured_content,不含任何日志痕迹;test_the_tool_logs_through_the_standard_library则用 pytest 的caplog捕获到一条名为docs_src.logging.tutorial001、级别为INFO、消息为Searching for 'dune'的标准库日志记录——证明工具内的日志走的就是普通 stdlib 通道。
日志去哪里:stdio 服务器下 stdout 与 stderr 的归属
对于stdio服务器,这个问题格外重要:宿主(host)把你的服务器作为子进程启动,并从其stdout上读取 MCP 协议消息。stderr 是你自己的。
标准库默认行为恰好正确:日志输出默认流向sys.stderr。你的logger.info(...)会落在终端(或宿主收集子进程 stderr 的地方),协议流保持干净。
不要在 stdio 服务器里print()
文档特别警告:不要在 stdio 服务器中使用print()。print写入stdout,而 stdout 属于协议。
SDK 对此有底层防护。从源码结构看,src/mcp/server/stdio.py 实现了一套“流认领(stream claim)”机制:stdio 传输启动时通过_claim_fd(1, sys.stdout, ...)认领 fd 1,并把协议链路放到一个私有 fd 副本上,原始 fd 被重定向到 stderr 一侧(_open_stdout_diversion()即os.dup(2),失败则退回/dev/null)。这意味着:
- 服务期间,真正被flush到 stdout 的内容会被 SDK 转移到 stderr,无法污染协议链路;
- 但在块缓冲(block-buffered)进程中,
print()的输出通常滞留在sys.stdout的缓冲区里,直到解释器退出时一次性冲刷——那一刻它会直接倒在协议流上; - 即便被转移了,这些行也是裸文本:没有级别、没有 logger 名、无法过滤,混杂在日志输出里。
相比之下,logger.debug("got here")是一行同样的工作量,且日志处理器会逐条 flush每条记录,去向也正确。
日志级别:log_level=参数与configure_logging的底层实现
你不需要自己调用logging.basicConfig()。构造MCPServer时 SDK 已经替你做了。从源码看这条调用链:
MCPServer.__init__的log_level参数定义为Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"],默认值"INFO"(见 src/mcp/server/mcpserver/server.py);- 服务器构建/启动时执行
configure_logging(self.settings.log_level)(server.py); configure_logging的实现在 src/mcp/server/mcpserver/utilities/logging.py:优先创建RichHandler(console=Console(stderr=True), rich_tracebacks=True)(handler 显式指向stderr,且带 rich 格式化与 traceback 美化),rich不可用时退回logging.StreamHandler(),最后调用logging.basicConfig(level=level, format="%(message)s", handlers=handlers)。
因此MCPServer("Bookshop", log_level="DEBUG")一行就足以让你看到logger.debug(...)的输出。
两条重要的规则性事实(均有测试覆盖):
logging.basicConfig()从不替换已存在的 handler。如果你在创建服务器之前自己配置了日志,你的配置优先——SDK 不会覆盖它。tests/docs_src/test_logging.py 中test_an_existing_logging_configuration_wins先给 root logger 装上NullHandler并置WARNING,再构造MCPServer("Bookshop", log_level="DEBUG"),断言级别与 handler 均未被改动;test_log_level_configures_the_root_logger则验证在无既有配置时 root 被设为DEBUG且恰好挂上 1 个 handler。- 不必在每个 handler 里
try/except只为记录失败。当工具或资源函数抛出异常时,SDK 会替你记录日志。记录内容与级别详见 错误处理(英文对照:Handling errors)。
动手验证:用 MCP Inspector 跑一遍
用 MCP Inspector 启动服务器:
uv run mcp dev server.py在Tools标签页调用search_books。Inspector 只展示返回值;日志行
Searching for 'dune'走的是 stderr——去终端,而不是协议链路。这正是上文“日志对模型不可见”的运行时验证方式。
需要的是“追踪”而非“日志”?
如果你真正想要的是追踪(每个请求、耗时、是否失败),那你要的不是日志行,而是spans。你的服务器已经在发出它们:SDK 默认用 OpenTelemetry 为每条消息打追踪。参见 OpenTelemetry(英文对照:OpenTelemetry)。
小结
- MCP 协议的日志能力已被 2026-07-28 规范弃用且无替代,不要在其上构建任何东西。
- 模块级
logger = logging.getLogger(__name__),工具内logger.info(...)——这就是完整模式。 - 日志输出永远不会到达模型,只有
return的值会。 - stderr 是你的,stdout 属于协议。服务期间 SDK 会把被 flush 的“越界” stdout 转移到 stderr,但未被 flush 的
print()仍可能在进程退出时倒在协议流上,且被转移的行没有任何标签;请使用logging——其处理器会逐条 flush。 MCPServer(..., log_level="DEBUG")设置级别;你先配置好的日志设置会被原样保留。- 需要“服务器有东西变了(工具列表、资源)通知客户端”时,那是 订阅(Subscriptions)(英文对照:subscriptions)的职责。
【免费下载链接】python-sdkThe official Python SDK for Model Context Protocol servers and clients项目地址: https://gitcode.com/gh_mirrors/pythonsd/python-sdk
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考