拆解 MCP(Model Context Protocol)的架构,关键不是罗列名词,而是看清它为什么非要拆成”Host / Client / Server”三方,而不是传统意义上”客户端 + 服务端”两方。三方模型让 AI 应用本身(Host)不用直接对接每一个外部工具(Server),中间的 Client 充当”安全守门员 + 协议翻译器”。Host 负责用户体验与 LLM 编排,Client 负责连接与消息路由,Server 负责把具体能力封装成标准原语。这种隔离设计是 MCP 能复用、能治理、能横向扩展的根因。
一、三方架构总览
MCP 把整个系统切成三个独立角色,每一个都只关心自己那一层的事。Host 是用户直接打交道的 AI 应用(Claude Desktop、VS Code + Copilot、自研 Agent 平台等),它运行 LLM 并决定什么时候调什么工具;Client 是 Host 内部维护的协议连接器,一个 Client 对应一个 Server,持有这条连接的状态机;Server 才是真正”干活的”,把数据库、文件系统、API、Git 仓库这些异构能力包成统一的 Tools / Resources / Prompts。
| 组件 | 部署位置 | 核心职责 | 典型实现 |
|---|---|---|---|
| Host | 终端用户应用 | UI、LLM 编排、用户授权、上下文聚合 | Claude Desktop、Cursor、自研 Agent |
| Client | Host 进程内 | 协议协商、消息路由、订阅管理、安全隔离 | MCP SDK 内置 Client、各语言客户端 |
| Server | 本地进程或远程服务 | 暴露 Tools / Resources / Prompts 原语 | GitHub MCP、FileSystem MCP、Database MCP |
MCP 规范明确要求 Host 与 Server 不直接通信:所有交互必须经过 Client 中转。这一条设计原则保证了 Server 拿不到 LLM 的全部对话历史,也拿不到别的 Server 的内部状态——Server 只能看到”我这次调用所需的最小上下文”,极大缩小了攻击面。
二、Host:用户体验与策略中心
Host 的存在感容易被低估,但它承担的职责其实最重。Host 要决定用户授权哪些工具、UI 上如何呈现工具调用结果、上下文窗口怎么分配,以及最关键的——在多 Client 并存时如何把不同 Server 的结果汇总成一次完整的 LLM 调用。
实际工程中,Host 通常用这样的初始化结构来管理多个 Client:
from mcp.client.session import ClientSession
from mcp.client.stdio import stdio_client
# Host 启动一个本地 MCP Server 子进程,并建立 Client 会话
async with stdio_client(["python", "github_mcp_server.py"]) as (read, write):
async with ClientSession(read, write) as session:
# 1. 能力协商:Client 声明自己支持的能力
await session.initialize()
# 2. 发现:列出 Server 暴露的工具
tools = await session.list_tools()
for t in tools:
print(t.name, "-", t.description)
效果与注意点:每多一个 Server,Host 就多起一个 Client,两者是 1:1 关系。Host 千万不能在 Client 之外另起一条”野连接”绕开协议——那样 Server 协议升级时整条链路会断。能力协商(capability negotiation)发生在 initialize 阶段,双方互相声明支持哪些原语,避免后续调用到一半才暴露能力缺失。
三、Client:协议守门员
Client 是三方中最”轻”却最关键的一环。它由 Host 创建,生命周期完全由 Host 管控,对外不暴露网络端口,对内也不直接处理业务逻辑。Client 的工作可以压缩成四件事:
- 与一个 Server 建立状态化会话(stateful session),保持连接心跳与重连;
- 在会话初始化时完成协议版本与能力协商;
- 双向路由 JSON-RPC 2.0 消息,包括 Requests / Responses / Notifications 三类;
- 维护订阅与通知机制,例如资源变更、工具列表更新。
3.1 能力协商:让协议可演进
MCP 不会一次性把”全部能力”都打开,而是按”声明 → 协商 → 使用”的顺序开放。Server 在 initialize 响应里声明自己支持 tools、resources、prompts 中的哪些,Client 则声明自己支持 sampling(让 Server 反向请 Client 调一次 LLM)或 roots(暴露文件系统根目录)。这种”能力开关”机制让协议能向后兼容地加新原语,旧实现不会因为新特性上线而崩。
下面这段展示 Server 端的初始化响应,能力声明一目了然:
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"protocolVersion": "2024-11-05",
"capabilities": {
"tools": { "listChanged": true },
"resources": { "subscribe": true, "listChanged": true },
"prompts": { "listChanged": true },
"logging": {}
},
"serverInfo": { "name": "github-mcp", "version": "1.0.0" }
}
}
Client 收到这份响应后,只把它声明的能力纳入可用集。如果想用 resources/subscribe,必须确认 resources.subscribe = true,否则会收到方法不存在的错误。
四、Server:能力的标准封装层
Server 是 MCP 落地的”重头戏”,所有具体能力都在这里被抽象成三种原语(Primitives):
- Tools:可执行函数,对应 LLM 的 Function Calling 场景,调用有 JSON Schema 约束的参数;
- Resources:可寻址的只读数据,通过 URI 标识,适合文件、文档、数据库记录等;
- Prompts:参数化的提示词模板,由 Server 注册、由 Host 触发(如
/generateApiRoute)。
用 Python 写一个最小 MCP Server 通常不到 50 行:
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("weather-mcp")
@mcp.tool()
async def get_weather(city: str) -> str:
"""根据城市名返回当前天气。"""
# 实际项目中对接气象 API,此处省略
return f"{city}: 晴,25°C"
if __name__ == "__main__":
# 本地进程通过 stdio 通信;远程部署可换 HTTP SSE
mcp.run(transport="stdio")
效果与注意点:@mcp.tool() 的文档字符串会直接成为 LLM 看到的工具描述,决定模型在什么时候选这个工具。描述写得越具体,模型挑得越准。@mcp.resource(uri=...) 用于暴露可读数据,@mcp.prompt() 用于注册可复用的提示词模板,三者不能混用。Server 永远不应主动”推消息”打断 LLM 推理,所有调用都应等 Client 来拉。
五、传输层:STDIO 与 HTTP SSE 的取舍
MCP 把传输层做成可插拔,目前主流是两种:
| 传输 | 通信方式 | 部署形态 | 适用场景 |
|---|---|---|---|
| STDIO | 进程间 stdin/stdout | 本地子进程 | 桌面应用、IDE 插件、本地开发 |
| HTTP SSE | 长连接 + 事件流 | 远程服务 | 分布式部署、云端、多租户 |
工程上有个反模式:把 STDIO Server 强行改造成 TCP 长连接再转发到远程。STDIO 假设 Server 与 Client 同生命周期,一旦跨网络就需要额外的鉴权、心跳与多路复用,不如直接走 HTTP SSE。工程上社区里较一致的推荐是:本地场景优先 STDIO,跨主机统一用 HTTP SSE。
到这里,Host 编排、Client 守门、Server 出力的三方协作链路就完整了。下一步要做的,是把这些能力映射到自己的业务系统里——先挑一两个高频工具做 MCP Server 验证,再逐步把数据源迁入。
常见问题(FAQ)
Q1:一个 Host 能连多少个 MCP Server?
理论上受限于 Host 的进程与上下文预算,常见做法是按业务域分组,每个 Host 维护 5–20 个活跃 Server。
Q2:MCP Server 必须用 Python 写吗?
不是。官方与社区提供 TypeScript、Python、Java、Kotlin、C#、Go、PHP、Rust 等多语言 SDK。
Q3:Client 与 Server 的连接断了会怎样?
Host 负责重连;未声明支持的能力在重连后不会自动恢复,需要重新做能力协商。