Agent 工程体系 · 第 32/98 篇。内容以 2026 年 9 月可验证的公开规范和稳定接口为基线;框架版本敏感能力会明确标注,不把实验行为写成通用保证。
MCP 架构深解:Host、Client、Server、能力协商和生命周期
MCP(Model Context Protocol)是一个用于连接 LLM 应用与外部数据源、工具和工作流的开放协议。它并不是“让模型直接访问 API”的函数调用格式,而是一套规定了组件边界、消息模型、能力声明、传输绑定、请求生命周期和安全责任的互操作协议。
在 MCP 中,最容易混淆的不是 tools/list 或 tools/call 的字段,而是下面几个问题:
- Host 和 Client 到底是不是同一个东西?
- 一个 Client 能否连接多个 Server?
- Server 是否能看到完整对话?
- “能力协商”是一次握手,还是每次请求都要携带?
- MCP 的“会话”是 TCP、HTTP、进程,还是业务上下文?
initialize还是否存在?- 一个工具调用失败时,应该返回 JSON-RPC error,还是返回
isError: true? - Server 能否主动调用 Client?
- Streamable HTTP 是否仍然需要粘性会话?
这些问题必须放在具体协议版本中回答。本文以 MCP 2026-07-28 规范为现代基线,同时解释截至 2025-11-25 及更早版本的兼容生命周期。2026-07-28 版本已经移除了协议层面的 initialize / initialized 握手和 Mcp-Session-Id,因此把旧版本的会话模型直接套到新版本上,会得到错误的架构判断。(modelcontextprotocol.io)
1. 先建立正确的架构模型
MCP 的核心关系可以表示为:
graph LR
U[用户] --> H[Host]
H --> L[LLM / Agent Runtime]
H --> C1[Client 1]
H --> C2[Client 2]
H --> C3[Client 3]
C1 <--> S1[Server A]
C2 <--> S2[Server B]
C3 <--> S3[Server C]
S1 --> E1[数据库 / 文件系统 / API]
S2 --> E2[代码执行环境]
S3 --> E3[企业业务系统]
其中:
- Host:承载 LLM 应用并负责协调的宿主进程。
- Client:Host 内部创建的 MCP 协议连接器。
- Server:对外暴露资源、提示模板和工具的 MCP 服务端。
- Transport:承载 JSON-RPC 消息的传输绑定,例如 stdio 或 Streamable HTTP。
- Capability:一方声明自己支持哪些协议功能。
- Primitive:MCP 中可被发现或调用的功能类型,例如 Tools、Resources 和 Prompts。
规范规定,一个 Host 可以管理多个 Client;每个 Client 与恰好一个 Server通信。多个 Server 的组合由 Host 完成,而不是由 Server 之间直接互相发现或调用。(modelcontextprotocol.io)
因此,下面两种说法都不准确:
“MCP Client 就是整个聊天应用。”
更准确的说法是:
聊天应用或 Agent Runtime 通常是 Host;其中每个指向一个 MCP Server 的协议连接器,才是一个 Client。
同样,下面这种结构也不是 MCP 的基本架构:
一个 Client -> 多个 Server
如果一个 Host 需要连接三个 Server,通常应当创建三个 Client:
Host
├── Client A -> Server A
├── Client B -> Server B
└── Client C -> Server C
这样设计的主要价值是隔离:
- 每个 Server 只接触发给它的请求。
- 一个 Server 不会自动看到另一个 Server 的上下文。
- 完整对话历史和跨 Server 编排逻辑保留在 Host。
- Host 可以为不同 Server 配置不同权限、凭据和用户确认策略。
2. Host:不是转发器,而是安全与语义边界
2.1 Host 的定义
Host 是启动或承载 AI 应用的容器和协调者。它通常负责:
- 创建、配置和销毁多个 Client;
- 决定哪些 Client 可以连接哪些 Server;
- 聚合来自多个 Server 的工具、资源和提示;
- 将可用能力转换为模型能够理解的上下文;
- 处理用户授权、确认和拒绝;
- 将模型的工具选择转换为具体的
tools/call请求; - 管理跨工具、跨 Server 的 Agent 流程;
- 处理模型采样和用户输入请求;
- 隔离不同 Server 之间的数据和权限。
MCP 规范明确把安全策略、用户授权、AI/LLM 集成和上下文聚合放在 Host 一侧。Server 的职责则应尽量聚焦于自身提供的能力。(modelcontextprotocol.io)
2.2 Host 为什么必须存在
假设一个 Agent 同时连接:
- 文件系统 Server;
- GitHub Server;
- 数据库 Server。
用户说:
“检查项目中的依赖漏洞,并创建一个 GitHub Issue。”
这个请求至少涉及:
- 文件系统读取;
- 依赖分析;
- GitHub API 写入;
- 可能的用户确认;
- 结果汇总。
如果让 Server 直接互相调用,会产生几个问题:
- GitHub Server 是否能读取文件系统 Server 的数据?
- 数据库 Server 是否能访问完整对话?
- 哪个组件决定“创建 Issue”是否需要用户批准?
- 不同 Server 的权限是否会被意外拼接?
- 失败后由谁负责重试和补偿?
Host 的存在把这些问题集中到一个可审计的位置:
sequenceDiagram
participant U as 用户
participant H as Host
participant L as LLM
participant F as 文件 Server
participant G as GitHub Server
U->>H: 检查漏洞并创建 Issue
H->>L: 提供任务、工具目录和上下文
L->>H: 选择文件分析工具
H->>F: tools/call
F-->>H: 漏洞分析结果
H->>L: 注入分析结果
L->>H: 选择 GitHub 创建工具
H->>U: 请求确认写入操作
U-->>H: 同意
H->>G: tools/call
G-->>H: Issue 创建结果
H-->>U: 汇总结果
这里的关键不是“Host 帮模型转发 JSON”,而是:
Host 决定哪些信息可以跨边界流动,以及哪些具有副作用的操作必须经过用户授权。
规范要求 Host 在暴露用户数据和调用工具前建立明确的同意与控制流程;工具应被视为可能执行任意代码的能力,工具描述和注解本身也不能在不可信来源下直接当作安全事实。(modelcontextprotocol.io)
3. Client:一条面向单个 Server 的协议连接
3.1 Client 的定义
Client 是由 Host 创建、负责与一个 Server 通信的连接器。它通常实现:
- MCP 消息编码和解码;
- 请求 ID 分配与响应匹配;
- 传输建立和关闭;
- Server 能力发现;
- 工具、资源、提示的列表获取;
- 通知接收;
- 取消、超时和重连;
- 请求级元数据注入;
- Server 返回结果的解析。
MCP Client 不是模型本身,也不是工具实现。它位于 Host 和 Server 之间:
LLM / Agent Runtime
|
| Host 内部调用
v
MCP Client
|
| JSON-RPC over transport
v
MCP Server
一个 Client 与一个 Server 的一对一关系,解决了两个隔离问题:
- 身份隔离:不同 Server 可以使用不同凭据和授权范围。
- 上下文隔离:Client 不应把一个 Server 的全部输入自动发送给另一个 Server。
3.2 Client 不等于连接生命周期
Client 对象可以比底层连接活得更久,也可以在一次连接断开后重新建立传输。反过来,一个长时间存在的 stdio 进程也不等于一个对话会话。
需要区分四种生命周期:
| 层次 | 例子 | 是否等价 |
|---|---|---|
| 进程生命周期 | 一个被 Host 启动的 Server 子进程 | 不等于会话 |
| 传输生命周期 | 一条 stdio 管道或 HTTP 请求流 | 不等于对话 |
| 协议请求生命周期 | 一个 tools/call 请求 |
通常独立 |
| 业务状态生命周期 | 一个任务 ID、游标、文件锁 | 由应用显式管理 |
现代 MCP 规范要求请求自描述,Server 不应依赖此前请求来推断协议版本、Client 身份或能力。需要跨多个请求保存的状态,必须通过显式标识符传递。(modelcontextprotocol.io)
因此,下列实现是错误的:
# 错误思路:假定上一次请求设置过用户
server.current_user = user_from_previous_request
# 下一次请求直接使用
def handle_tool_call(arguments):
return operate_as(server.current_user, arguments)
正确做法是把业务状态显式编码到请求参数中:
{
"name": "continue_task",
"arguments": {
"taskId": "task_123",
"page": 2
}
}
这样,即使请求被负载均衡到另一台 Server 实例,也能根据 taskId 恢复业务状态。
4. Server:能力提供者,而不是 Agent 编排器
4.1 Server 的定义
Server 是向 Client 提供上下文和能力的服务端。它可以暴露:
- Tools:模型或 Host 可以调用的函数;
- Resources:可读取的上下文和数据;
- Prompts:提示模板或工作流入口;
- 以及规范或扩展定义的其他能力。
Server 可以是:
- Host 启动的本地子进程;
- 通过网络访问的远程服务;
- 企业内部网关后的服务;
- 由 SDK 封装的业务模块。
MCP 只规范 Server 与 Client 之间的协议,不规定 Server 内部必须使用什么语言、数据库或框架。官方 SDK 当前提供 TypeScript、Python、C#、Go、Rust 等 Tier 1 SDK,以及 Java、Ruby、Swift、PHP、Kotlin 等其他 SDK 层级。不同 SDK 遵循各自语言习惯,但都围绕 Server、Client 和标准传输提供实现。(modelcontextprotocol.io)
4.2 Server 不应该知道什么
一个合规的 Server 通常不应默认知道:
- 用户的完整聊天记录;
- Host 连接的其他 Server;
- 模型使用了哪些内部思维或中间状态;
- 用户没有授权的数据;
- 与当前请求无关的其他任务上下文。
这不是“Server 永远不能接收这些数据”,而是:
Server 只能接收 Host 明确放进当前请求的数据。
例如,Host 可以向工具传入:
{
"name": "search_code",
"arguments": {
"repository": "org/project",
"query": "timeout",
"scope": "src/"
}
}
但不应因为调用了 search_code,就把完整聊天记录、其他 Server 的返回值和所有用户文件一起发送过去。
5. JSON-RPC:MCP 的消息基础
MCP 消息使用 JSON-RPC 2.0 编码。规范要求 Client 和 Server 之间的消息遵循 JSON-RPC 结构。核心消息类型有三种:
- Request:需要响应的请求;
- Response:成功结果或错误;
- Notification:不需要响应的单向通知。(modelcontextprotocol.io)
5.1 Request
{
"jsonrpc": "2.0",
"id": 42,
"method": "tools/call",
"params": {
"name": "get_weather",
"arguments": {
"location": "Hangzhou"
}
}
}
请求 ID 必须满足两个条件:
- 必须是字符串或整数;
- 不能与尚未收到响应的其他请求 ID 重复。
因此,Client 可以并发发出多个请求:
id=41 -> tools/list
id=42 -> tools/call
id=43 -> resources/read
只要响应携带相同 ID,Client 就能正确匹配:
response id=43 -> resources/read
response id=41 -> tools/list
response id=42 -> tools/call
响应顺序不必等于请求顺序。
5.2 Result Response
成功响应具有:
{
"jsonrpc": "2.0",
"id": 42,
"result": {
"resultType": "complete",
"content": [
{
"type": "text",
"text": "杭州当前天气晴朗"
}
],
"isError": false
}
}
在 2026-07-28 规范中,resultType 用于区分结果形态:
complete:请求已经完成;input_required:请求尚未完成,需要 Client 提供额外输入;- 扩展可以增加其他结果类型,但必须通过能力声明进行支持协商。
旧版本没有 resultType 时,兼容 Client 应将缺失值按 "complete" 处理。(modelcontextprotocol.io)
5.3 JSON-RPC Error 与工具业务错误
这是生产实现中最常见的错误分类混淆。
协议或请求层错误
例如:
- JSON 格式损坏;
- method 不存在;
- 参数结构不合法;
- 不支持的协议版本;
- 请求缺少必要的 Client 能力。
这种情况使用 JSON-RPC error:
{
"jsonrpc": "2.0",
"id": 42,
"error": {
"code": -32602,
"message": "Invalid params",
"data": {
"field": "location",
"reason": "must be a string"
}
}
}
工具执行层错误
工具本身被正确识别、参数也符合 Schema,但外部业务操作失败,例如:
- GitHub 返回权限不足;
- 数据库连接超时;
- 查询结果为空;
- 删除操作被业务规则拒绝。
这通常仍然是一个合法的工具结果,只是结果中的 isError 为 true:
{
"jsonrpc": "2.0",
"id": 42,
"result": {
"resultType": "complete",
"isError": true,
"content": [
{
"type": "text",
"text": "GitHub API 返回 403:当前令牌没有创建 Issue 的权限"
}
]
}
}
判断逻辑可以写成:
JSON-RPC error
=> 请求没有按 MCP 方法完成,通常属于协议、路由或参数层失败
result.isError = true
=> MCP 调用本身完成,但工具业务执行失败
如果把所有业务异常都包装成 JSON-RPC error,Host 很难区分“工具不存在”和“工具执行失败”;如果把参数解析失败都包装成 isError: true,Client 可能误以为工具已经正常运行。
6. 能力协商:不是“发现工具列表”的同义词
6.1 Capability 的含义
Capability 是一方对协议功能支持范围的声明。
例如,Server 可能声明:
{
"capabilities": {
"tools": {
"listChanged": true
},
"resources": {
"subscribe": true
}
}
}
这表示:
- Server 支持工具相关方法;
- 工具列表发生变化时,可以发送相应通知;
- Server 支持资源订阅。
Client 也可以声明自己的能力,例如是否支持:
- sampling;
- elicitation;
- roots;
- 某个可选扩展。
能力声明不是对“业务权限”的完整描述。它只说明协议功能是否存在;是否允许当前用户使用,还要经过授权和 Host 策略。
6.2 能力的有效集合
可以用一个简化模型表示某个功能最终是否可用。
设:
- :Server 已实现的能力;
- :Client 已声明的能力;
- :双方协议版本支持的能力;
- :授权和 Host 策略允许的能力;
- :可选扩展双方都明确启用的能力。
则当前请求的有效能力集合可以表示为:
但不同功能的条件并不完全相同。
例如,Server 提供工具的必要条件主要是:
如果工具调用需要 Server 请求用户补充信息,则还需要:
这说明:
“Server 宣布了 tools 能力”不等于“任何用户都能调用所有工具”。
6.3 2026-07-28 的请求级能力模型
现代规范要求每个请求携带协议版本和 Client 能力等元数据,典型形式为:
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/list",
"params": {
"_meta": {
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
"io.modelcontextprotocol/clientInfo": {
"name": "example-host",
"version": "1.0.0"
},
"io.modelcontextprotocol/clientCapabilities": {}
}
}
}
这里的关键变化是:Server 不能只看连接建立时保存的状态来判断当前请求,而应读取当前请求中的 _meta。
规范要求:
- 必要的请求元数据必须存在;
- Server 不得依赖 Client 未声明的能力;
- 如果请求需要某项未声明的 Client 能力,Server 应返回
-32021; clientInfo和serverInfo是自报告信息,不能用于安全决策。(modelcontextprotocol.io)
6.4 server/discover 的作用
在现代协议中,客户端可以调用 server/discover,预先获取 Server 能力。
这一步是可选的前置发现,不是旧式 initialize 的简单改名:
Client -> server/discover
Server -> DiscoverResult
Client -> tools/list
Server -> tools
Client -> tools/call
Server -> result
如果 Client 不需要预先获取完整能力,也可以直接发送普通请求,因为每个请求已经携带了自身需要的协议元数据。架构规范将 server/discover 描述为可以在其他请求之前调用的能力发现机制。(modelcontextprotocol.io)
7. 工具发现不是“把函数名发给模型”
工具发现至少包含四个层次:
- Server 是否声明了
tools能力; - Client 是否成功执行
tools/list; - 每个工具是否有合法的输入 Schema;
- Host 是否愿意把这些工具暴露给模型或用户。
典型工具定义:
{
"name": "search_code",
"title": "搜索代码",
"description": "在指定仓库范围内搜索代码文本",
"inputSchema": {
"type": "object",
"properties": {
"query": {
"type": "string",
"minLength": 1
},
"scope": {
"type": "string"
}
},
"required": ["query"],
"additionalProperties": false
}
}
inputSchema 是 JSON Schema,不是给模型看的普通文档。它至少承担三项职责:
- 告诉模型应该生成什么参数;
- 让 Client 在调用前验证参数;
- 让 Server 在执行前再次验证参数。
MCP 2026-07-28 默认支持 JSON Schema 2020-12。Server 提供的 Schema 必须合法,Client 和 Server 都应按声明的方言进行验证。对于外部 $ref,实现不得默认通过网络自动解引用,因为这可能引入 SSRF、供应链和拒绝服务风险。(modelcontextprotocol.io)
7.1 为什么需要双重验证
假设模型产生:
{
"query": 123,
"scope": "../"
}
Client 侧验证发现 query 不是字符串,可以在网络请求前拒绝。
但即使 Client 验证通过,Server 仍然必须验证,因为:
- Client 可能存在实现缺陷;
- 请求可能来自非标准 Client;
- Host 可能错误地拼装了参数;
- 攻击者可能直接调用 Server;
- Schema 只能表达结构,不能表达全部安全约束。
因此安全边界应当是:
模型输出
-> Host 策略检查
-> Client Schema 校验
-> 传输
-> Server Schema 校验
-> 业务授权
-> 工具执行
7.2 tools/list 的确定性与缓存
现代规范允许工具列表携带缓存提示,例如:
{
"resultType": "complete",
"tools": [],
"ttlMs": 300000,
"cacheScope": "public"
}
Server 在底层工具集合没有变化时,应保持稳定顺序。确定性排序可以让 Client 缓存工具目录,也可以减少工具被注入模型上下文时的无意义变化。工具集合仍可能因请求携带的授权信息不同而不同,因此不能把所有用户看到的工具目录简单视为全局公共缓存。(modelcontextprotocol.io)
8. 两种生命周期:旧式握手与现代无会话请求
MCP 生命周期必须按协议时代区分。
8.1 2025-11-25 及更早版本:连接级握手
旧式生命周期如下:
stateDiagram-v2
[*] --> Created
Created --> TransportConnected: 建立 stdio/HTTP
TransportConnected --> Initializing: initialize
Initializing --> Initialized: 返回 InitializeResult
Initialized --> Ready: notifications/initialized
Ready --> Calling: tools/list 或 tools/call
Calling --> Ready: result/error
Ready --> Closing: close 或传输断开
Closing --> [*]
典型流程:
1. 建立传输
2. Client 发送 initialize
3. Server 返回:
- protocolVersion
- capabilities
- serverInfo
4. Client 发送 notifications/initialized
5. 开始 tools/list、resources/list、tools/call
6. 连接关闭时结束协议会话
旧版本中,协议版本和能力主要在 initialize 阶段协商,并被绑定到该连接或会话。Streamable HTTP 还会通过 Mcp-Session-Id 将后续请求关联到已建立的协议会话。
8.2 2026-07-28:无握手、无协议会话
现代生命周期可以表示为:
stateDiagram-v2
[*] --> ClientConfigured
ClientConfigured --> Discovering: 可选 server/discover
Discovering --> RequestReady: 得到能力
ClientConfigured --> RequestReady: 直接发请求
RequestReady --> InFlight: 发送独立请求
InFlight --> Completed: result
InFlight --> ProtocolError: JSON-RPC error
InFlight --> Cancelled: stdio 通知或 HTTP 关闭响应流
Completed --> RequestReady: 继续其他请求
ProtocolError --> RequestReady: 按错误策略处理
Cancelled --> RequestReady: 可重试或终止
现代请求自身携带:
- 协议版本;
- Client 能力;
- 可选 Client 身份;
- 当前请求的参数;
- 需要传递的业务状态标识。
因此,协议层不再依赖:
“这个请求来自哪条连接?”
“这个连接之前是否成功 initialize?”
“这个 HTTP 请求是否被路由到同一台机器?”
2026-07-28 版本移除了 initialize / initialized 和 Mcp-Session-Id,使得任意请求可以被负载均衡到任意 Server 实例,而不需要共享协议会话存储。(blog.modelcontextprotocol.io)
8.3 “无会话”不等于“业务无状态”
这是现代 MCP 最容易被误读的地方。
无会话的含义是:
协议层不要求通过连接 ID 保存版本、能力和 Client 身份。
它不表示:
- 数据库没有事务;
- 工具不能创建任务;
- Server 不能保存缓存;
- 文件系统没有锁;
- 业务不能产生任务句柄。
例如,一个异步导入工具可以返回:
{
"resultType": "complete",
"structuredContent": {
"taskId": "import_789",
"status": "accepted"
}
}
后续请求显式携带:
{
"name": "get_import_status",
"arguments": {
"taskId": "import_789"
}
}
业务状态由 taskId 表示,而不是由 HTTP 连接或 stdio 进程表示。
9. MRTR:现代协议中的多轮请求
旧式 MCP 允许 Server 主动向 Client 发 JSON-RPC 请求,例如 Server 请求 Client 进行 sampling 或 elicitation。这要求传输层具备持续的双向请求能力。
现代 MCP 使用 Multi Round-Trip Requests,MRTR 表示多轮请求。Server 不直接在协议层另起一个独立的 Server-to-Client JSON-RPC 请求,而是在结果中说明:
{
"jsonrpc": "2.0",
"id": 10,
"result": {
"resultType": "input_required",
"inputRequests": {
"confirm_scope": {
"method": "elicitation/create",
"params": {
"mode": "form",
"message": "请选择允许扫描的目录",
"requestedSchema": {
"type": "object",
"properties": {
"scope": {
"type": "string"
}
},
"required": ["scope"]
}
}
}
},
"requestState": "opaque-server-state"
}
}
Client 获取用户输入后,用新的请求 ID 重试:
{
"jsonrpc": "2.0",
"id": 11,
"method": "tools/call",
"params": {
"name": "scan_project",
"arguments": {
"scope": "src/"
},
"inputResponses": {
"confirm_scope": {
"action": "accept",
"content": {
"scope": "src/"
}
}
},
"requestState": "opaque-server-state"
}
}
这里有两个重要约束:
- 重试请求必须使用新的 JSON-RPC ID;
requestState是 Server 生成的业务状态,不应被 Client 修改或解释。
MCP 工具规范将这种模式用于需要额外用户输入的工具调用。(modelcontextprotocol.io)
10. stdio 与 Streamable HTTP:相同语义,不同绑定
Transport 只负责消息如何传输,不改变 MCP 消息本身的语义。当前标准传输主要有:
- stdio:Client 启动 Server 子进程,通过标准输入输出交换换行分隔的 JSON-RPC 消息;
- Streamable HTTP:每条消息通过发往 MCP Endpoint 的 HTTP POST 发送,响应可以是 JSON 对象,也可以是请求作用域的 SSE 流。(modelcontextprotocol.io)
10.1 stdio
stdio 的典型拓扑:
Host
|
| 创建子进程
v
MCP Server Process
stdin <- Client requests
stdout -> Server responses/notifications
stderr -> logs
一个重要边界是:
stdout 只能输出协议消息,日志应输出到 stderr。
否则,普通日志会破坏 JSON-RPC 的消息边界:
这是调试日志
{"jsonrpc":"2.0", ...}
Client 无法判断第一行是否是 MCP 消息。
stdio 的优点:
- 本地部署简单;
- 不需要 HTTP 端口;
- 进程权限容易隔离;
- 适合桌面应用、IDE 和本地开发工具。
风险是:
- Server 进程崩溃会直接导致传输断开;
- 重启后不能假设旧进程状态仍存在;
- 环境变量、工作目录和继承凭据必须显式控制;
- 不应把“进程还活着”当成“业务会话还有效”。
按照现代规范,stdio 上的取消通过 notifications/cancelled 通知表达;传输层的其他语义仍由 MCP 核心协议定义。(modelcontextprotocol.io)
10.2 Streamable HTTP
现代 Streamable HTTP 的基本形态是:
Client
|
| POST /mcp
| MCP-Protocol-Version: 2026-07-28
| Mcp-Method: tools/call
| Mcp-Name: search_code
v
Load Balancer / Proxy
|
+--> Server Instance A
+--> Server Instance B
+--> Server Instance C
规范语义上,消息体中的 _meta 是来源;HTTP 头可以镜像部分请求元数据,用于路由和中间件处理。这样网关可以在不解析完整 JSON Body 的情况下,根据 Mcp-Method、Mcp-Name 或工具参数对应的 Mcp-Param-* 头进行路由。(modelcontextprotocol.io)
一个现代请求可以写成:
curl -i https://mcp.example.com/mcp \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-H 'MCP-Protocol-Version: 2026-07-28' \
-H 'Mcp-Method: tools/call' \
-H 'Mcp-Name: search_code' \
--data-binary @- <<'JSON'
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "search_code",
"arguments": {
"query": "timeout",
"scope": "src/"
},
"_meta": {
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
"io.modelcontextprotocol/clientInfo": {
"name": "example-host",
"version": "1.0.0"
},
"io.modelcontextprotocol/clientCapabilities": {}
}
}
}
JSON
前置条件:
- Endpoint 必须实现 Streamable HTTP;
- Server 必须提供名为
search_code的工具; - 工具 Schema 必须接受
query和scope; - HTTP 层需要完成认证和授权;
- 网关不能修改与请求体冲突的镜像头。
现代 Streamable HTTP 的关键收益是:协议请求不再依赖 Mcp-Session-Id。因此普通负载均衡即可工作,但这并不意味着业务状态可以随意放在单机内存中。只要业务请求跨请求关联,就必须使用显式任务 ID、资源 URI、游标或其他可持久化标识。
11. 传输取消、超时和重连
取消不是普通错误。
11.1 stdio 取消
在 stdio 上,Client 可以发送:
{
"jsonrpc": "2.0",
"method": "notifications/cancelled",
"params": {
"requestId": 42,
"reason": "client_timeout"
}
}
这是 Notification,因此没有响应。
Server 收到后应尽快停止对应操作,但不能假设底层数据库、子进程或远程 API 一定已经同步终止。取消通常是协作式的:
Client 发送取消
-> Server 设置取消标志
-> 工具检查取消标志
-> 终止可取消的下游操作
-> 释放资源
如果工具没有检查取消点,协议层取消不会自动中断任意阻塞系统调用。
11.2 Streamable HTTP 取消
在现代 Streamable HTTP 上,Client 取消正在进行的请求时,可以关闭该请求的响应流。关闭流本身就是传输层取消信号。(modelcontextprotocol.io)
因此,不能把 stdio 的取消逻辑机械复制到 HTTP:
stdio:
发送 notifications/cancelled
现代 Streamable HTTP:
关闭当前请求的响应流
11.3 重试的危险
对于只读操作,超时后通常可以重试:
tools/list
resources/read
查询类 tools/call
但对于写操作,超时并不代表服务端没有执行。例如:
Client -> 创建订单
连接超时
Client -> 重试创建订单
可能得到两个订单。
正确的工程做法不是简单地“所有 MCP 请求自动重试”,而是区分:
- 只读操作;
- 幂等写操作;
- 非幂等写操作;
- 带业务幂等键的写操作。
MCP 协议本身不能替业务系统定义幂等语义。若工具支持,应将幂等键显式纳入参数:
{
"name": "create_issue",
"arguments": {
"repository": "org/project",
"title": "Dependency vulnerability",
"idempotencyKey": "host-run-20260901-001"
}
}
12. 工具结果:文本、结构化数据和资源引用
工具结果可以同时包含面向模型或用户的非结构化内容,以及供程序处理的结构化内容。
{
"resultType": "complete",
"content": [
{
"type": "text",
"text": "找到 2 个高危依赖"
}
],
"structuredContent": {
"count": 2,
"severity": "high",
"packages": [
{
"name": "example-lib",
"version": "1.2.3"
}
]
}
}
如果工具提供 outputSchema:
{
"outputSchema": {
"type": "object",
"properties": {
"count": {
"type": "integer",
"minimum": 0
},
"severity": {
"type": "string",
"enum": ["low", "medium", "high", "critical"]
}
},
"required": ["count", "severity"],
"additionalProperties": false
}
}
则:
- Server 必须返回符合 Schema 的
structuredContent; - Client 应验证返回结果;
structuredContent与 LLM 的“结构化输出”不是同一个概念;- 现代规范允许
structuredContent是任意 JSON 值,包括数组、字符串、数字和null; - 为兼容旧 Client,返回结构化内容时通常还应提供序列化后的 TextContent。(modelcontextprotocol.io)
工具也可以返回资源引用:
{
"type": "resource_link",
"uri": "file:///project/reports/audit.json",
"name": "audit.json",
"mimeType": "application/json"
}
这比把大型报告直接塞进文本结果更适合:
- 大文件;
- 可订阅资源;
- 需要后续
resources/read的内容; - 需要保留 MIME 类型和资源元数据的场景。
13. 现代 MCP 中的安全边界
MCP Server 连接的是外部系统,因此工具不是普通的“纯函数”。
13.1 工具描述不是安全策略
工具定义中的:
{
"description": "删除用户数据,但这是安全的"
}
只是描述,不是授权证明。
同样,工具注解中即使声明:
{
"annotations": {
"readOnlyHint": true
}
}
Host 也不应在不可信 Server 上把它当作事实。工具实际行为必须由 Server 权限、Host 策略和业务审计共同约束。
13.2 用户确认应位于 Host
对于以下操作,Host 通常应提供明确的用户确认:
- 删除文件;
- 写入数据库;
- 发送邮件;
- 创建或关闭工单;
- 执行 shell 命令;
- 修改生产配置;
- 上传用户数据。
确认界面至少应展示:
Server:github.example
Tool:create_issue
Repository:org/project
Title:Dependency vulnerability
Action:创建公开 Issue
不能只显示:
允许调用工具吗?
因为用户需要理解实际副作用,而不是理解抽象的 RPC 名称。
13.3 Server 不能看到完整上下文
Host 应按照最小必要原则构造请求。对于一个工具调用,传入:
{
"query": "timeout",
"scope": "src/"
}
通常比传入完整对话更安全。
如果工具确实需要用户身份或授权上下文,应通过明确的请求字段、认证凭据或受控元数据传递,而不是依赖“Server 记得这个连接之前是谁”。
13.4 x-mcp-header 的边界
现代工具 Schema 可以声明某个原始类型参数映射到 HTTP Header:
{
"region": {
"type": "string",
"x-mcp-header": "Region"
}
}
调用时:
{
"region": "us-west1"
}
可以被镜像成:
Mcp-Param-Region: us-west1
适合路由、分区或网关策略判断,但不适合:
- 密码;
- API Key;
- OAuth Token;
- 身份证号;
- 其他敏感个人信息。
因为 HTTP Header 可能被负载均衡器、WAF、代理和日志系统记录。规范也限制了该注解只能用于原始类型参数,并要求 Client 拒绝不合法的 Header 映射。(modelcontextprotocol.io)
14. 常见误解与对应失败表现
误解一:Host、Client、Server 是三个网络进程
不一定。
常见部署可能是:
Host 进程
├── Client A:进程内对象
└── Client B:进程内对象
Server A:子进程
Server B:远程 HTTP 服务
Client 通常是 Host 内的协议对象,而不是独立进程。
误解二:一个连接就是一个用户会话
在现代 MCP 中,这是错误的。
可能出现:
同一条 HTTP 连接:
请求 1 属于任务 A
请求 2 属于任务 B
同一个任务:
请求 1 到实例 A
请求 2 到实例 C
如果 Server 把连接 ID 当作用户或任务 ID,就会出现跨用户数据污染、任务状态丢失和负载均衡后的“偶发失败”。
误解三:调用 tools/list 就完成了能力协商
不完整。
tools/list 只发现工具目录。它不能替代:
- 协议版本判断;
- Client 能力声明;
- Server 能力判断;
- 扩展启用;
- Host 权限和用户确认。
误解四:没有 initialize 就没有生命周期
错误。
现代生命周期只是从:
连接 -> 握手 -> 会话 -> 请求
变为:
请求配置 -> 可选发现 -> 独立请求 -> 结果
连接、请求、业务任务和资源仍然各自存在生命周期,只是协议不再用一次握手保存全部上下文。
误解五:Server 返回 JSON-RPC error 就代表工具执行失败
不一定。
如果工具不存在、参数无效或版本不支持,可能是 JSON-RPC error;如果工具已经开始执行业务但业务失败,更适合返回合法工具结果并设置 isError: true。
误解六:Streamable HTTP 仍然需要粘性会话
对于 2026-07-28 现代协议,不再需要协议层 Mcp-Session-Id 粘性会话。可是业务状态仍需显式持久化。去掉粘性会话不等于可以把任务状态只放在单台机器的内存里。
15. 一个完整的调用推导
考虑一个 get_weather 工具。
15.1 Server 发布工具
{
"capabilities": {
"tools": {
"listChanged": false
}
}
}
工具定义:
{
"name": "get_weather",
"description": "查询指定城市天气",
"inputSchema": {
"type": "object",
"properties": {
"location": {
"type": "string",
"minLength": 1
}
},
"required": ["location"],
"additionalProperties": false
},
"outputSchema": {
"type": "object",
"properties": {
"temperatureC": {
"type": "number"
},
"condition": {
"type": "string"
}
},
"required": ["temperatureC", "condition"],
"additionalProperties": false
}
}
15.2 Host 决策
Host 检查:
Server 声明 tools = 是
工具出现在 tools/list = 是
输入 Schema 合法 = 是
当前用户允许查询天气 = 是
工具是只读查询 = 是
需要用户确认 = 否
因此允许把工具提供给模型。
15.3 模型生成调用参数
{
"location": "Hangzhou"
}
Client 在发送前验证:
类型是 object = 是
location 是 string = 是
location 非空 = 是
没有额外字段 = 是
15.4 Server 再次验证并执行
Server 收到请求后再次验证,调用天气 API,然后返回:
{
"jsonrpc": "2.0",
"id": 7,
"result": {
"resultType": "complete",
"content": [
{
"type": "text",
"text": "杭州:23.5°C,晴"
}
],
"structuredContent": {
"temperatureC": 23.5,
"condition": "晴"
},
"isError": false
}
}
15.5 API 失败时
假设天气 API 返回 503:
{
"jsonrpc": "2.0",
"id": 7,
"result": {
"resultType": "complete",
"content": [
{
"type": "text",
"text": "天气服务暂时不可用,请稍后重试"
}
],
"isError": true
}
}
这表示:
MCP 请求已被识别
工具名称有效
参数结构有效
工具执行流程完成
外部业务调用失败
而不是:
MCP 方法不存在
16. 生产诊断应该沿着哪条路径进行
遇到 MCP 调用失败时,不要先看模型输出,而应沿协议路径逐层定位:
Host 策略
-> Client 能力与版本
-> Transport 建连
-> JSON-RPC 编码
-> Server 路由
-> capability 检查
-> Schema 校验
-> 认证与授权
-> 工具业务执行
-> Result / Error
-> Host 结果解释
可以按以下顺序检查。
16.1 传输层
- stdio Server 是否启动成功?
- stdout 是否混入日志?
- HTTP Endpoint 是否返回正确的 Content-Type?
- 代理是否截断 SSE 流?
- 请求是否被超时取消?
- HTTP Header 与 Body 中的协议元数据是否冲突?
16.2 协议层
- JSON-RPC 是否包含
jsonrpc、id和method? - 请求 ID 是否重复?
- 当前协议版本是否被 Server 支持?
- 现代请求是否包含必需的
_meta? - 是否错误地向现代 Server 发送了旧版
initialize?
16.3 能力层
- Server 是否声明
tools? - Client 是否声明了 Server 要求的能力?
- 使用的扩展是否由双方明确支持?
tools/list返回的工具是否为空,还是工具被权限过滤?
16.4 Schema 层
inputSchema是否为合法 JSON Schema?$schema方言是否受支持?- 必填参数是否缺失?
- 是否存在额外字段?
- 输出是否符合
outputSchema? - 是否因为非法
x-mcp-header导致工具被 Client 排除?
16.5 业务层
- Token 是否有对应 scope?
- 当前用户是否有操作权限?
- 工具执行是否超时?
- 重试是否可能造成重复写入?
- 业务状态是否错误地绑定在单机内存或连接对象上?
17. 版本迁移时最重要的判断
以 2026-09 的 Agent 工程基线为例,应当同时面对两类 Server:
| 能力 | 旧版本 Server | 现代 Server |
|---|---|---|
| 协议打开方式 | initialize / initialized |
无握手 |
| 版本携带方式 | 初始化阶段协商 | 每个请求携带 |
| 能力携带方式 | 初始化阶段协商 | 每个请求携带 |
| HTTP 会话 | 可能使用 Mcp-Session-Id |
已移除 |
| Server 到 Client 请求 | 旧式双向 JSON-RPC | 现代 MRTR 结果 |
| 负载均衡 | 可能需要粘性会话 | 协议层可无状态路由 |
兼容 Client 的合理策略是:
- 优先探测现代能力;
- 如果 Server 不支持现代协议,再回退到旧式
initialize; - 在后续请求中严格使用已选择的协议时代;
- 不要把现代请求字段与旧式会话假设混用;
- 对不同协议版本分别测试取消、错误码、通知和重连行为。
官方 SDK 已提供多语言实现,但具体 SDK 的版本选择、默认协商策略和 API 名称可能随 SDK 主版本变化,不能仅凭某个语言 SDK 的示例推断所有 SDK 都具有相同的默认行为。协议语义应以对应版本的规范为准,SDK API 则应以该 SDK 的版本文档为准。(modelcontextprotocol.io)
MCP 的核心不是“把工具注册给模型”,而是建立一条受约束的能力通道:
Host 决定边界
Client 负责协议连接
Server 提供专门能力
Capability 限制可用功能
Schema 限制数据形状
Transport 负责消息传递
Lifecycle 定义请求如何开始、完成、取消和结束
在旧版 MCP 中,生命周期围绕连接级握手和协议会话展开;在 2026-07-28 现代基线中,生命周期转向请求级自描述和显式业务状态。理解这一变化,才能正确设计无粘性会话的 HTTP 部署、可靠的重试机制、跨实例任务、用户授权流程,以及同时兼容新旧 Server 的 Agent Host。
系列导航与关联阅读
- 系列入口:Agent 工程完整路线:从运行循环、记忆与协议到安全、评测和生产交付
- 上一篇:Agent 知识引用:证据片段、来源映射、冲突和可验证回答
- 下一篇:MCP Tools:发现、Schema、调用、结果、错误和安全边界
- 延伸:MCP 传输:stdio、Streamable HTTP、会话、重连和代理
官方资料
本文依据 Agent、模型、协议与框架官方资料重新梳理;正文、示例与生产清单由 WR BLOG 编写。

评论
0 条讨论