MCP Python SDK 日志实践:标准库 logging、log_level 配置与 stdio 进程的 stdout/stderr 分流
2026/9/20 19:01:23 网站建设 项目流程

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_clientinline_snapshot精确断言了call_tool返回的CallToolResult只包含contentstructured_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 已经替你做了。从源码看这条调用链:

  1. MCPServer.__init__log_level参数定义为Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]默认值"INFO"(见 src/mcp/server/mcpserver/server.py);
  2. 服务器构建/启动时执行configure_logging(self.settings.log_level)(server.py);
  3. 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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询