8.2 stdio 传输:子进程与标准输入输出


文档摘要

8.2 stdio 传输:子进程与标准输入输出 本节摘要:stdio 是 MCP 的本地默认传输,也是 Claude Desktop、IDE 这类本地宿主用的方式。核心机制很直接:宿主把你的服务端文件当成一个子进程启动,用它的标准输入(stdin)和标准输出(stdout)当 MCP 消息的「线缆」。本节讲透它的工作机制、启动方式、环境变量的「白名单」特性,以及为什么它适合本地场景却不能用于远程。 一、stdio 的工作机制 stdio 传输的核心思想:把子进程的标准输入输出当消息线缆。 宿主用类似 的命令启动你的服务端文件,把它作为子进程。

8.2 stdio 传输:子进程与标准输入输出

本节摘要:stdio 是 MCP 的本地默认传输,也是 Claude Desktop、IDE 这类本地宿主用的方式。核心机制很直接:宿主把你的服务端文件当成一个子进程启动,用它的标准输入(stdin)和标准输出(stdout)当 MCP 消息的「线缆」。本节讲透它的工作机制、启动方式、环境变量的「白名单」特性,以及为什么它适合本地场景却不能用于远程。

一、stdio 的工作机制

stdio 传输的核心思想:把子进程的标准输入输出当消息线缆

宿主(Claude Desktop) 你的服务端(子进程) ┌──────────────────┐ ┌──────────────────┐ │ │ stdin ──► │ │ │ 客户端 │ │ MCPServer │ │ │ ◄── stdout │ │ │ │ │ │ └──────────────────┘ └──────────────────┘

宿主用类似 python server.py 的命令启动你的服务端文件,把它作为子进程。然后:

  • 宿主把要发的消息写到子进程的 stdin
  • 子进程把响应写到自己的 stdout,宿主读它

子进程就像一个「会说话的命令行程序」——输入是 MCP 请求,输出是 MCP 响应。

二、启动方式

服务端用 mcp.run() 启动 stdio(默认传输):

from mcp.server import MCPServer mcp = MCPServer("Demo") @mcp.tool() def add(a: int, b: int) -> int: """Add two numbers.""" return a + b if __name__ == "__main__": mcp.run() # 默认跑 stdio 传输

mcp.run() 不传参数时,默认用 stdio。它会:

  1. 把 stdin/stdout 接到 MCP 读写流
  2. 进入事件循环,开始接受消息
  3. 阻塞,直到连接关闭

宿主端(客户端)则用 StdioServerParameters 指定怎么启动这个子进程(第 10 章详讲客户端侧)。

三、为什么用 stdin/stdout

为什么选 stdin/stdout 当线缆?几个理由:

理由 说明
零配置 不用开端口、不用配地址,启动子进程就行
跨平台 所有操作系统都有 stdin/stdout
进程隔离 子进程崩溃不影响宿主,重启即可
无网络 本地通信,不占端口、无网络安全问题
简单 宿主只需「启动子进程 + 读写它的 stdio」

这种「子进程 + stdio」的模式,在开发者工具里很常见——很多语言服务器(LSP)、格式化工具、 lint 工具都用类似机制。MCP 借鉴了这个成熟模式。

四、环境变量的白名单特性

stdio 有一个容易踩坑的特性——子进程拿到的不是你的完整环境,而是白名单环境变量

你的 shell 环境: PATH=/usr/bin:... OPENAI_API_KEY=sk-xxx ← 敏感 DATABASE_URL=... ← 敏感 HOME=/home/user 子进程(默认): PATH=/usr/bin:... ← 只拿「安全」的白名单变量 (OPENAI_API_KEY、DATABASE_URL 不传)

为什么这样?安全。宿主不希望你的服务端子进程无意中拿到宿主的所有敏感环境(如 API Key)。默认只传「必需且安全」的白名单(如 PATH)。

如果你的服务端需要特定环境变量(如数据库连接串),要显式注入:

# 客户端侧启动子进程时显式注入(概念性) stdio_params = StdioServerParameters( command="python", args=["server.py"], env={ # 显式注入需要的环境变量 "DATABASE_URL": "...", "API_KEY": "...", } )

⚠️ 注意:别假设子进程能拿到你 shell 的所有变量。如果你的服务端依赖某个环境变量(如 os.environ["DATABASE_URL"]),必须显式注入,否则子进程里 KeyError。这是 stdio 传输最常见的「为什么我服务端跑不起来」的原因之一。

五、stdio 适合的场景

stdio 的特性决定了它适合的场景:

特性 适合 不适合
本地子进程 ✅ 本地工具(Claude Desktop、IDE) ❌ 远程服务
无端口 ✅ 不占网络资源 ❌ 跨机器访问
进程隔离 ✅ 崩溃可重启 ❌ 多客户端共享
stdin/stdout ✅ 单一消息流 ❌ 并发多请求(需排队)

stdio 最适合「一个宿主 + 一个本地服务端」的场景——如 Claude Desktop 连你写的本地数据库工具。这种场景下,stdio 简单、安全、零配置,是首选。

六、stdio 的局限

stdio 也有局限,主要是:

局限 说明
不能远程 子进程要在宿主机启动,不能跨机器
单连接 一个子进程的 stdio 只能服务一个宿主
无并发 stdin/stdout 是单一消息流,并发请求需排队
生命周期绑定 宿主关,子进程也关

这些局限决定了 stdio 不适合「生产级服务」——你需要多个客户端并发访问、跨机器部署时,要用流式 HTTP(下一节)。

七、stdio 的调试技巧

stdio 因为是子进程,调试有几个技巧:

技巧 说明
stderr 不受影响 服务端的 print 到 stderr 不会被宿主当消息(只有 stdout 是线缆),可用于调试
用日志文件 服务端写本地日志文件,排查问题
检查器优先 开发期用 mcp dev(第 1.4 节)看 stdio 行为
环境变量排查 「服务端跑不起来」先查环境变量是否注入

💡 技巧:stdout 是 MCP 线缆,别 print 到 stdout。如果你在服务端 print("debug info"),这条信息会被宿主当成 MCP 消息解析,导致协议错误。调试输出用 print(..., file=sys.stderr) 或标准 logging(默认到 stderr)。

本节要点回顾

  1. stdio 是本地默认传输,宿主把服务端文件当子进程启动,用 stdin/stdout 当线缆。
  2. 启动用 mcp.run()(默认 stdio),阻塞进入事件循环。
  3. 选 stdin/stdout 的理由:零配置、跨平台、进程隔离、无网络、简单。
  4. 环境变量白名单:子进程默认只拿安全变量,需要敏感变量要显式注入。
  5. 适合场景:本地工具(Claude Desktop、IDE),一宿主一服务端。
  6. 局限:不能远程、单连接、无并发、生命周期绑定宿主。
  7. 调试:stdout 是线缆别 print 到 stdout,调试用 stderr 或日志文件。

stdio 清楚了,下一节讲流式 HTTP——生产级传输,2025-03-26 主力。


发布者: 作者: 灏天文库 转发
评论区 (0)
U