3784 字
约 12 分钟
13
8.4 能力协商与版本发现
2026-10-25
无标签

8.4 能力协商与版本发现

一句话结论:一个协议如果不能演进,就会死。版本发现与能力协商让不同版本的客户端与服务端可以共存——客户端不必预先知道服务端是什么,问一句就知道。

读这篇之前

需要先读 8.2(两层结构)。这一篇讲 MCP 里最像"基础设施"的部分。


一、没有版本协商会怎样

假设 MCP 只有一种形态,且不协商版本。会发生什么?

场景 后果
服务端升级加了新工具 老客户端不认识,可能报错
客户端升级用了新字段 老服务端解析失败
服务端只支持部分能力 客户端调用不支持的功能,失败

这就是所有协议都做版本协商的原因——不是洁癖,是生存需要。


二、MCP 的做法:问一次就知道

数据层定义了一个关键请求:server/discover——客户端通过它查询服务端支持的协议版本、能力与身份。[核验:MCP 官方架构]

客户端 → server/discover
服务端 ← {
  protocolVersion: "2026-07-28",
  capabilities: {
    tools: {...},
    resources: {...},
    prompts: {...}
  },
  serverInfo: { name: "...", version: "..." }
}

三个信息各自解决一个问题:

信息 解决什么
protocolVersion 双方用哪个版本的规则说话
capabilities 服务端支持哪些原语、有哪些子特性
serverInfo 这是谁(用于日志、诊断、权限策略)

三、能力协商的实际价值

价值一:渐进式演进

服务端可以只实现一部分能力。比如只做 tools 不做 resources——客户端通过 capabilities 就知道,不会去调不存在的功能。

价值二:特性开关

即使都支持 tools,也可能在子特性上有差异(比如是否支持流式、是否支持批量)。capabilities 可以携带这些细节。

价值三:每个请求自带上下文

一个值得注意的设计细节:每个请求在其 _meta 字段中携带协议版本与该请求相关的能力,服务端可以独立处理每个请求。[核验:官方架构]

为什么这样设计? 因为 MCP 可能跑在无状态传输上(如 Streamable HTTP)。如果没有"每请求自带上下文",服务端就必须维护会话状态——那会牺牲可扩展性。

这是一个典型的分布式系统取舍:把状态放进消息里,而不是放在服务端内存里。


四、与 HTTP 协商的类比

熟悉 HTTP 的人会觉得这个模式眼熟:

MCP HTTP
server/discover OPTIONS / Accept 头
protocolVersion HTTP/2 vs HTTP/1.1 协商
capabilities 服务端声明的支持特性

核心思想完全一致:先问对方能做什么,再决定怎么说话。


五、实践中的四个注意点

# 注意 后果
1 客户端要处理"服务端不支持" 否则调用失败
2 版本不匹配要有降级路径 硬失败会中断整个 Agent
3 发现结果要缓存但可失效 每次都问浪费延迟
4 身份信息用于权限策略 不同服务端应有不同信任等级

第 4 条容易被忽略但很重要:服务端身份(serverInfo)应该参与你的权限判断。一个"自己写的本地服务端"和一个"从网上装的第三方服务端",信任等级显然不同。



六、动手:版本不匹配怎么办

协商机制设计了,实际会遇到不兼容。三种情况三种处理。

情况一:客户端版本更高,服务端较旧

← {"protocolVersion":"2025-06-18", ...}   ← 服务端只支持旧版

处理:客户端降级到旧版语义,或明确拒绝并提示用户升级服务端。

做法 优点 缺点
自动降级 兼容性好 客户端要维护多版本逻辑
拒绝并提示 简单、明确 用户体验差

建议:核心能力做降级,新特性缺失时告警而非失败。

情况二:服务端声明不支持某原语

← {"capabilities":{"tools":{}}, ...}    ← 只支持 tools,没有 resources

处理:客户端不要尝试调用 resources。这是最容易被忽略的一点——很多客户端硬编码假设"一定有 resources"。

情况三:_meta 里带了不支持的能力

→ {"jsonrpc":"2.0","id":5,"method":"tools/call",
   "_meta":{"protocolVersion":"2026-07-28","capabilities":["resources"]},
   "params":{...}}

处理:服务端应忽略不认识的能力,而不是报错。这保证了向前兼容。


七、一个容易被低估的实践:缓存发现结果

server/discover 每次都问会很浪费(尤其是 stdio 每次 spawn 进程)。

缓存策略 适用
进程内缓存(连接期间) stdio,连接稳定
带 TTL 的缓存(如 5 分钟) HTTP,服务端可能升级
不缓存 调试期

要能失效:服务端升级后,客户端必须能在下一次会话拿到新能力。


八、为什么"每请求自带 _meta"很重要(展开)

官方架构里提到:每个请求在 _meta 携带协议版本与相关能力,服务端可以独立处理每个请求。[核验]

这个设计的价值在无状态部署:

有状态 无状态(每请求带 _meta)
连接期间记录"这个客户端支持什么" 每个请求自带
不能随意负载均衡 可任意分发到任一实例
实例重启丢失会话 无影响

这是分布式系统的基本取舍:把状态放进消息,服务端就容易横向扩展。



九、协商失败的处理矩阵

协商不通过时,不同失败类型要有不同反应:

失败类型 例子 正确反应
完全不能通信 版本差太远 明确报错,提示升级
能力缺失 没有 resources 降级:只用 tools
字段不认识 新字段 忽略,继续
服务端报错 server/discover 失败 重试 → 标记不可用

最忌讳的是"悄悄降级"——用户不知道某项能力没生效。

差:连接成功但静默少了 3 个工具
好:连接成功,日志与 UI 提示「服务端不支持 resources,已降级」

这与 10.5 的可观测性原则一致:状态变化要可见。


十、一个实践建议:把协商结果当作能力清单

把 server/discover 的结果缓存成一张能力表,供后续所有决策使用:

能力表(启动时构建)
  服务端 A:tools ✅  resources ✅  prompts ❌
  服务端 B:tools ✅  resources ❌  prompts ✅

用途:

决策 依据
界面显示哪些功能 能力表
工具清单怎么生成 各服务端 tools 的并集
出错时怎么提示 能力表 + 报错类型
是否降级 缺哪个能力

这张表比"每次现问"更高效,也比"硬编码假设"更可靠。


补充误区

误区七:版本协商只在连接时做一次。
无状态传输下,每个请求都要带版本信息。

误区八:客户端可以假设服务端支持全部原语。
必须按 capabilities 判断。

误区九:不认识的能力应该报错。
应该忽略,否则无法向前兼容。

误区十:发现结果可以永久缓存。
服务端会升级,必须能失效。


十一、落地时的版本管理策略

协议协商解决"运行时兼容",但你还需管理"发布时的版本"。

策略 做法 适用
语义化版本 主版本不兼容、次版本加能力、补丁修 bug 通用
向后兼容优先 新能力放可选,不删旧字段 长期服务
双版本并行 同时暴露 v1/v2,给客户端迁移期 大改动
弃用告警 旧能力标记 deprecated,返回告警 渐进淘汰

最关键的一条:加能力容易,删能力难。

安全:加一个新工具、加一个可选参数 → 老客户端不受影响
危险:删一个工具、改一个字段类型 → 老客户端直接崩

所以设计时的原则是:多留可选字段,少删已有字段。


十二、常见误区

误区一:版本协商是形式主义。
没有它,协议演进就意味着全生态断代。

误区二:客户端可以硬编码服务端能力。
那就失去了插拔性。必须动态发现。

误区三:发现结果可以永久缓存。
服务端可能升级。要能失效。

误区四:所有服务端应同等信任。
不同来源的服务端信任等级不同(见 5.4 关于 Skill 的提醒)。

误区五:无状态传输和版本协商冲突。
恰恰相反——每请求自带 _meta 正是为了支持无状态。


十三、动手:能力发现的完整交互

8.4 讲了机制,这里看它怎么用。

一次完整的协商

① 客户端 → server/discover
② 服务端 ← {protocolVersion, capabilities, serverInfo}
③ 客户端据此构建"能力表"
④ 后续所有决策都查这张表

能力表的三种用途

用途 怎么用
界面 只显示服务端支持的功能
工具清单 取各服务端 tools 的并集
降级判断 缺哪个能力就走哪条备用路径

一个具体的降级示例

caps = discover(server)

if "resources" in caps:
    # 优先用 resources(客户端可控暴露范围,更安全)
    ctx = load_resources(server)
else:
    # 降级为工具读取(权限更松,需要额外校验)
    ctx = call_tool(server, "read_context")

# 但一定要让用户知道降级了
log.warning(f"{server} 不支持 resources,已降级为工具读取")

最后那行日志很重要 —— 悄悄降级会让用户以为一切正常,而实际上安全模型变松了。

一个反面案例

错:客户端硬编码假设"所有服务端都支持 resources"
    → 遇到只支持 tools 的服务端直接崩

MCP 的能力协商就是为了消除这种假设。 不用它,等于白定协议。


十四、动手:能力协商的完整实现

8.4 的机制落到代码,就是"发现 → 缓存 → 查询"三步。

class CapabilityTable:
    """缓存各服务端的能力,供后续所有决策使用"""

    def __init__(self):
        self.table = {}          # server -> {version, caps, info}
        self.fetched_at = {}

    def discover(self, server, force=False):
        if not force and server in self.table and self._fresh(server):
            return self.table[server]
        raw = call(server, "server/discover", {})
        self.table[server] = {
            "version": raw["protocolVersion"],
            "caps": raw["capabilities"],
            "info": raw["serverInfo"],
        }
        self.fetched_at[server] = time.time()
        return self.table[server]

    def has(self, server, cap):
        return cap in self.table.get(server, {}).get("caps", {})

    def _fresh(self, server, ttl=300):
        return time.time() - self.fetched_at.get(server, 0) < ttl

三个设计要点

要点 为什么
带 TTL 的缓存 每次都问浪费;永久缓存会错过升级
has() 查询接口 让后续代码不直接读原始结构
force=True 可刷新 服务端升级后能重新发现

一个典型用法

caps = table.discover("github")
if table.has("github", "resources"):
    ctx = load_resources("github")
else:
    log.warning("github 不支持 resources,改用 tools 读取(权限更松)")
    ctx = call_tool("github", "read_context")

中间那行日志不能省 —— 悄悄降级会让用户以为安全模型没变。


十五、一个反例:把能力当"常量"写死

# 错:假设所有服务端都支持 resources
ctx = load_resources(server)

后果:遇到只支持 tools 的服务端(很常见),直接抛异常,整个任务中断。

正确做法:

ctx = load_resources(server) if table.has(server, "resources") \
      else call_tool(server, "read_context")

这就是 MCP 定义 server/discover 的意义 —— 让客户端能适配未知服务端,而不是把所有服务端当成同一种。


十六、一个补充:协商失败该不该"硬失败"

失败类型 硬失败 降级
协议版本差太远(无法通信) ✅ 硬失败 ——
缺 resources 但只需要 tools —— ✅ 降级
缺 tools(根本无法工作) ✅ 硬失败 ——
某个工具不可用 —— ✅ 跳过该工具

判据:

缺的这个能力,是"必须的吗"?
  必须 → 硬失败,明确报错
  可选 → 降级,但要告知

最忌讳的两种极端:

极端 后果
一律硬失败 一个小能力缺失导致整个系统不可用
一律静默降级 用户不知道功能少了,事后才发现

正确的中间态是"降级 + 显式告知",这也符合 10.5 的可观测性原则。


16、下一篇

原语和协商都有了。还剩一个 MCP 里最能体现"工程成熟度"的设计:双向协议与懒连接。

它解决的是"服务端也需要反过来问客户端"这类问题,以及"启动时不要把所有连接都建立起来"的实际困难。

下一篇讲这两点。

十四、一个有价值的副产品:能力表可以驱动 UI

协商结果不只能驱动逻辑,还能驱动界面。

能力表内容 界面上可以
tools 存在 显示"可用工具"区域
resources 存在 显示"资料浏览"入口
prompts 存在 显示"快捷模板"菜单
某服务不可用 灰显对应入口 + 提示原因

这比硬编码界面强得多:新接一个服务端,界面自动多出对应入口,不用改前端。

这也是为什么"能力发现"不只是协议细节,而是产品能力的来源。

一句收尾

协商不是握手仪式,而是"让客户端能适配它从没见过的服务端"的机制。用不用它,决定了你的实现能不能进生态。

8.4 能力协商与版本发现
https://clxhxhhr.top/posts/4905/
作者
clxstart
发布于
2026-10-25
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。