MCP 架构:Host、Client、Server 三角色
搞清楚 MCP 里 Host、Client、Server 各自干什么、怎么连接、消息怎么流转。
搞清楚 MCP 的两种消息传输方式——本地用 stdio、远程用 Streamable HTTP,以及怎么选。
内容摘要
在前面两张卡片里我们已经知道:MCP 的数据层用 JSON-RPC 2.0 定义了"消息长什么样",而传输层要解决的是另一个问题——**这些 JSON 消息怎么从 Client 送到 Server、再送回来**。
在前面两张卡片里我们已经知道:MCP 的数据层用 JSON-RPC 2.0 定义了"消息长什么样",而传输层要解决的是另一个问题——这些 JSON 消息怎么从 Client 送到 Server、再送回来。
MCP 目前定义了两种标准传输方式:
| 传输方式 | 一句话 | 适用场景 |
|---|---|---|
| stdio | Client 启动一个子进程,通过标准输入/输出管道交换 JSON 消息 | 本地 Server(Filesystem、Git、SQLite 等跑在你电脑上的程序) |
| Streamable HTTP | Client 通过 HTTP POST 发送消息,Server 可以用 SSE 流式返回 | 远程 Server(Sentry、Cloudflare 等跑在云端的服务) |
用一个类比:stdio 像面对面说话——你和对方在同一个房间里,声音直接传过去,不需要电话线、不需要网络。Streamable HTTP 像打电话——对方在远方,你需要拨号、等接通、有可能断线重连,但好处是距离不是问题。
两种传输方式之上的 JSON-RPC 消息格式是完全一样的。换句话说,同一个 Server 的业务代码不用改一行,只要切换启动方式就能从本地切到远程。
stdio 是最简单、最常用的传输方式。流程如下:
关键规则:
print("debug") 之类的调试信息一个真实的 stdio 消息交换看起来是这样的(每行一条 JSON):
→ stdin: {"jsonrpc":"2.0","id":1,"method":"initialize","params":{...}}
← stdout: {"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2025-06-18",...}}
→ stdin: {"jsonrpc":"2.0","method":"notifications/initialized"}
→ stdin: {"jsonrpc":"2.0","id":2,"method":"tools/list"}
← stdout: {"jsonrpc":"2.0","id":2,"result":{"tools":[...]}}
Streamable HTTP 是 MCP 2025 年引入的新传输方式(替代了旧版的 HTTP+SSE transport)。它用一个单一的 HTTP 端点(比如 https://example.com/mcp)同时处理 POST 和 GET 请求:
关键机制:
application/json 和 text/event-stream 两种响应格式Mcp-Session-Id,后续所有请求都需要带上这个 IDid 字段,断线后 Client 用 Last-Event-Id 请求头恢复,Server 从断点续传2024-11-05 版本的 MCP 协议用的是另一种传输方式:Server 先通过一个 SSE 端点返回一个 POST URL,Client 再往那个 URL 发消息。这种方式已经被 Streamable HTTP 替代,但为了向后兼容:
stdio 不需要鉴权(因为 Server 就是 Host 启动的本地子进程,天然可信)。Streamable HTTP 面对的是公网环境,所以需要鉴权:
| 鉴权方式 | 说明 |
|---|---|
| OAuth 2.1 | MCP 推荐的标准方式,适合企业级远程 Server |
| Bearer Token | 在 HTTP Authorization 头里传 token |
| API Key | 作为查询参数或自定义请求头传递 |
此外,远程 Server 还需要注意:
Origin 请求头,防止 DNS 重绑定攻击在 Claude Desktop 的 claude_desktop_config.json 里配一个本地 Server,用的就是 stdio:
{
"mcpServers": {
"filesystem": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/Users/me/Desktop"]
}
}
}
Host 看到 command + args 就知道:启动这个命令作为子进程,通过 stdin/stdout 通信。不需要写 transport: "stdio",因为有 command 字段的默认就是 stdio。
连接一个远程 Server,比如某个部署在云端的 MCP 服务:
{
"mcpServers": {
"remote-service": {
"url": "https://mcp.example.com/mcp",
"headers": {
"Authorization": "Bearer your-api-token-here"
}
}
}
}
Host 看到 url 字段就知道:这是一个远程 Server,通过 Streamable HTTP 通信。headers 用于传鉴权信息。
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("my-server")
@mcp.tool()
def hello(name: str) -> str:
"""Say hello"""
return f"Hello, {name}!"
# stdio 模式启动(本地开发)
# 命令行运行:python server.py
if __name__ == "__main__":
mcp.run() # 默认 stdio
# 如果要切换成 HTTP 模式(远程部署),只需改启动方式:
# mcp.run(transport="streamable-http", host="0.0.0.0", port=8000)
注意看:Tool 的代码一行都不用改,只是启动方式从 mcp.run() 变成 mcp.run(transport="streamable-http")。这就是数据层和传输层分离的好处。
| 维度 | MCP stdio | MCP Streamable HTTP | gRPC | REST API | WebSocket |
|---|---|---|---|---|---|
| 通信方向 | 双向(stdin/stdout) | 双向(POST + SSE) | 双向 | 单向(请求-响应) | 双向 |
| 网络需求 | 无(本地管道) | 需要 HTTP | 需要 HTTP/2 | 需要 HTTP | 需要 TCP |
| 流式支持 | 逐行 JSON | SSE 流 | 原生流 | 不支持 | 原生流 |
| 鉴权 | 不需要 | OAuth / Bearer / API Key | mTLS / Token | 任意 | 任意 |
| 协议开销 | 极低 | HTTP 头开销 | Protobuf 编码开销 | HTTP 头开销 | 帧头开销 |
| 适合场景 | 本地工具 | 远程服务 | 微服务间通信 | 通用 API | 实时通信 |
核心区别:
| 误区 | 准确理解 |
|---|---|
| 以为 stdio 只能传文本 | stdio 传的是 JSON-RPC 消息(UTF-8 编码的 JSON),工具返回的图片/音频数据用 base64 编码后也能通过 JSON 在 stdio 里传输 |
| 以为 Streamable HTTP 就是 REST API | 不是。REST 是请求-响应模式,而 Streamable HTTP 支持 SSE 流式推送和 GET 长连接,Server 可以主动给 Client 发通知。更接近"HTTP 上的双向消息通道" |
| 以为用了 HTTP transport 就不需要初始化握手了 | 不管用什么 transport,MCP 都需要先完成 initialize → initialized 的生命周期握手。HTTP transport 的 Mcp-Session-Id 就是握手后分配的 |
以为 Server 的 print() 可以随便用 | 在 stdio 模式下,stdout 是消息管道!如果 Server 里有 print("debug"),这条文本会混入 JSON 消息流,直接搞坏协议。调试信息必须写到 stderr |
| 以为旧的 HTTP+SSE transport 还能用 | 新版 MCP(2025-03-26 起)官方已用 Streamable HTTP 替代旧版。旧版仍可向后兼容,但新 Server 应该只实现 Streamable HTTP |
| 优势 | 劣势 |
|---|---|
| stdio 极简:不需要网络、不需要鉴权、不需要端口,Host 启动子进程就完事,调试只需看 stdin/stdout | stdio 无法远程:Host 和 Server 必须在同一台机器上,不能跨网络调用 |
| Streamable HTTP 全能:支持流式、断线恢复、Session 管理、OAuth 鉴权,企业级特性齐全 | Streamable HTTP 复杂度高:要处理 Session ID、SSE 流管理、断线重连、Origin 验证等,实现成本远高于 stdio |
| 传输无关的数据层:换 transport 不需要改业务代码,一份 Server 逻辑同时适配本地和远程 | 没有官方的 WebSocket/gRPC transport:对于需要低延迟双向实时通信的场景,社区实现还不成熟 |
| 向后兼容设计:新 Client 有标准的降级流程来连接旧版 HTTP+SSE Server | stdio 的 print 陷阱:新手在 Server 里写了 print() 导致协议被污染,这是最常见的调试痛点 |
参考答案:
因为在 stdio 传输中,Server 进程的 stdout 是和 Client 通信的管道。Client 会把 stdout 上收到的每一行都当作 JSON-RPC 消息来解析。如果 Server 代码里有一句 print("debug info"),这段文本会被 Client 当成 JSON 来解析,直接报错——因为 "debug info" 根本不是合法的 JSON-RPC 消息。
正确做法:调试信息写到 stderr(import sys; print("debug", file=sys.stderr)),或者用 MCP 的 Logging 原语发日志给 Client。Python SDK 的 FastMCP 框架会自动帮你把 Python 的 logging 模块重定向到 stderr,所以用 logging.debug() 是安全的。
参考答案:
MCP 的分层设计天然支持这个需求。关键思路是把业务逻辑(注册 Tool / Resource / Prompt)和传输层启动方式分开。
以 Python SDK 为例:
from mcp.server.fastmcp import FastMCP
import sys
mcp = FastMCP("my-server")
# 业务逻辑——不关心传输方式
@mcp.tool()
def my_tool(input: str) -> str:
return f"processed: {input}"
# 启动方式——根据参数切换 transport
if __name__ == "__main__":
if "--http" in sys.argv:
mcp.run(transport="streamable-http", host="0.0.0.0", port=8000)
else:
mcp.run() # 默认 stdio
本地开发时 python server.py 走 stdio,部署时 python server.py --http 走 HTTP。Tool 注册代码一行不改。
更成熟的做法是用环境变量(MCP_TRANSPORT=http)或者配置文件来切换,然后在 CI/CD 里根据环境自动选择。Docker 镜像里通常默认启动 HTTP 模式。
参考答案:
Streamable HTTP 的断线恢复基于 SSE 规范的 id 和 Last-Event-ID 机制:
id(在当前 Session 内全局唯一)Last-Event-ID 请求头里带上最后收到的事件 ID这个机制本质上是在 SSE 流上实现了一个游标(cursor),让流可以从任意点恢复。
stdio 不需要这个机制,因为 stdio 是进程内管道,两端始终在同一台机器的同一个操作系统里。管道要么存在(进程活着),要么不存在(进程退了),不存在"断一下再重连"的中间状态。如果子进程崩了,Host 会重新 spawn 一个新进程,从头开始建立连接,不存在"从上次断点续传"的需求。
优先展示同分类且标签更接近的内容,方便继续串联学习。
搞清楚 MCP 里 Host、Client、Server 各自干什么、怎么连接、消息怎么流转。
搞清楚 MCP Server 能暴露的三种能力——工具、资源、提示词模板,以及什么时候该用哪个。
MCP 的关键不是“连上就能用”,而是先走严格生命周期:版本协商、能力交换、initialized 通知,然后才能进入正常通信。