8.2 stdio 传输:子进程与标准输入输出 本节摘要:stdio 是 MCP 的本地默认传输,也是 Claude Desktop、IDE 这类本地宿主用的方式。核心机制很直接:宿主把你的服务端文件当成一个子进程启动,用它的标准输入(stdin)和标准输出(stdout)当 MCP 消息的「线缆」。本节讲透它的工作机制、启动方式、环境变量的「白名单」特性,以及为什么它适合本地场景却不能用于远程。 一、stdio 的工作机制 stdio 传输的核心思想:把子进程的标准输入输出当消息线缆。 宿主用类似 的命令启动你的服务端文件,把它作为子进程。
本节摘要:stdio 是 MCP 的本地默认传输,也是 Claude Desktop、IDE 这类本地宿主用的方式。核心机制很直接:宿主把你的服务端文件当成一个子进程启动,用它的标准输入(stdin)和标准输出(stdout)当 MCP 消息的「线缆」。本节讲透它的工作机制、启动方式、环境变量的「白名单」特性,以及为什么它适合本地场景却不能用于远程。
stdio 传输的核心思想:把子进程的标准输入输出当消息线缆。
宿主(Claude Desktop) 你的服务端(子进程) ┌──────────────────┐ ┌──────────────────┐ │ │ stdin ──► │ │ │ 客户端 │ │ MCPServer │ │ │ ◄── stdout │ │ │ │ │ │ └──────────────────┘ └──────────────────┘
宿主用类似 python server.py 的命令启动你的服务端文件,把它作为子进程。然后:
子进程就像一个「会说话的命令行程序」——输入是 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。它会:
宿主端(客户端)则用 StdioServerParameters 指定怎么启动这个子进程(第 10 章详讲客户端侧)。
为什么选 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 的特性决定了它适合的场景:
| 特性 | 适合 | 不适合 |
|---|---|---|
| 本地子进程 | ✅ 本地工具(Claude Desktop、IDE) | ❌ 远程服务 |
| 无端口 | ✅ 不占网络资源 | ❌ 跨机器访问 |
| 进程隔离 | ✅ 崩溃可重启 | ❌ 多客户端共享 |
| stdin/stdout | ✅ 单一消息流 | ❌ 并发多请求(需排队) |
stdio 最适合「一个宿主 + 一个本地服务端」的场景——如 Claude Desktop 连你写的本地数据库工具。这种场景下,stdio 简单、安全、零配置,是首选。
stdio 也有局限,主要是:
| 局限 | 说明 |
|---|---|
| 不能远程 | 子进程要在宿主机启动,不能跨机器 |
| 单连接 | 一个子进程的 stdio 只能服务一个宿主 |
| 无并发 | stdin/stdout 是单一消息流,并发请求需排队 |
| 生命周期绑定 | 宿主关,子进程也关 |
这些局限决定了 stdio 不适合「生产级服务」——你需要多个客户端并发访问、跨机器部署时,要用流式 HTTP(下一节)。
stdio 因为是子进程,调试有几个技巧:
| 技巧 | 说明 |
|---|---|
| stderr 不受影响 | 服务端的 print 到 stderr 不会被宿主当消息(只有 stdout 是线缆),可用于调试 |
| 用日志文件 | 服务端写本地日志文件,排查问题 |
| 检查器优先 | 开发期用 mcp dev(第 1.4 节)看 stdio 行为 |
| 环境变量排查 | 「服务端跑不起来」先查环境变量是否注入 |
💡 技巧:stdout 是 MCP 线缆,别 print 到 stdout。如果你在服务端
print("debug info"),这条信息会被宿主当成 MCP 消息解析,导致协议错误。调试输出用print(..., file=sys.stderr)或标准 logging(默认到 stderr)。
mcp.run()(默认 stdio),阻塞进入事件循环。stdio 清楚了,下一节讲流式 HTTP——生产级传输,2025-03-26 主力。