结论:MCP 从 2026-07-28 修订起取消了握手式协商——每个请求在 _meta 里自带协议版本、客户端身份与能力,服务端逐条接受或拒绝。想提前知道服务端支持什么,就调一次强制实现的 server/discover;不想多一次往返,直接发请求、收到 UnsupportedProtocolVersionError 再从返回的 supported 列表里挑一个版本重试即可。
两个时代:legacy 与 modern
按官方术语,协议被分成两类行为族:
| 时代 | 版本范围 | 连接方式 | 关键特征 |
|---|---|---|---|
| Legacy | 2025-11-25 及更早(含 2024-10-07) | initialize 握手 + Mcp-Session-Id | 有会话状态、双向流 |
| Modern | 2026-07-28 及之后 | 无握手,server/discover 可选探测 | 每请求自描述、可无状态 |
| Dual-era | 两者都支持 | 按探测结果切换 | 兼容期主流实现 |
版本号是 YYYY-MM-DD 格式,表示”最后一次不兼容变更的日期”;向后兼容的改动不会递增版本号。当前版本是 2026-07-28。
版本协商:没有协商握手
这是最容易误解的一点。新版里不存在版本协商握手,流程是这样的:
- 客户端在请求的
_meta里带上io.modelcontextprotocol/protocolVersion;HTTP 传输同时用MCP-Protocol-Version头承载同一个值。 - 服务端逐条判断:支持就处理,不支持就返回错误码
-32022的UnsupportedProtocolVersionError,并在data.supported里列出自己支持的版本。 - 客户端从
supported里选一个双方都支持的版本重试;一个都没有就向用户报错。
{
"jsonrpc": "2.0",
"id": 1,
"error": {
"code": -32022,
"message": "Unsupported protocol version",
"data": {
"supported": ["2026-07-28", "2025-11-25"],
"requested": "1900-01-01"
}
}
}
server/discover:可选的提前探测
如果不想”先撞墙再重试”,可以在发业务请求前调一次 server/discover——这是服务端必须实现的 RPC,一次返回支持的协议版本、能力和身份信息:
// 请求
{ "jsonrpc": "2.0", "id": 0, "method": "server/discover", "params": {} }
// 响应(示意)
{
"jsonrpc": "2.0",
"id": 0,
"result": {
"protocolVersions": ["2026-07-28", "2025-11-25"],
"capabilities": { "tools": {}, "resources": {} },
"serverInfo": { "name": "my-server", "version": "2.1.0" }
}
}
调用它是可选的,但有几个实际好处:一次性拿到能力清单、避免第一次业务请求的失败往返、把探测结果缓存下来供后续复用。
扩展协商:capabilities 里的 extensions 字段
核心协议之外的可选能力,通过 capabilities.extensions 这个 map 声明,键是扩展标识符(必须带前缀),值是该扩展的设置对象。空对象表示”支持但无额外配置”。
// 客户端声明支持 MCP Apps 扩展
{
"capabilities": {
"roots": {},
"extensions": {
"io.modelcontextprotocol/ui": {
"mimeTypes": ["text/html;profile=mcp-app"]
}
}
}
}
// 服务端声明支持 Tasks 扩展
{
"capabilities": {
"tools": {},
"extensions": { "io.modelcontextprotocol/tasks": {} }
}
}
规则很明确:一方支持、另一方不支持时,支持方必须回退到核心协议行为,或者用合适的错误拒绝请求。扩展规范应当写明预期回退行为。这意味着你在实现扩展时必须想清楚”对面不支持时怎么办”,否则对面会直接报错。
目前已正式化的扩展包括 MCP Apps(UI)、 Tasks 、 Enterprise Managed Authorization 等。
客户端侧怎么选时代
TypeScript SDK 提供三种模式,对应不同的兼容性策略:
mode: 'legacy'(默认):直接走 2025 握手,零探测,行为与旧版一致。mode: 'auto':先用server/discover探测,探测不到现代服务端就回退到 initialize 握手(多一次往返,但不报错)。mode: { pin: '2026-07-28' }:只认这个版本,探测失败直接抛ERA_NEGOTIATION_FAILED,绝不回退。
import { Client, StreamableHTTPClientTransport } from '@modelcontextprotocol/client';
const client = new Client(
{ name: 'my-client', version: '1.0.0' },
{ versionNegotiation: { mode: 'auto' } }
);
await client.connect(new StreamableHTTPClientTransport(new URL('http://localhost:3000/mcp')));
console.log(client.getProtocolEra()); // 'modern' | 'legacy'
四个实现要点
- 时代判定属于服务端,不属于单次请求:客户端应在进程生命周期内缓存探测结论,不要每个请求都探一次。
- 鉴权状态不能当作时代证据:401/403 必须当成鉴权失败处理,不能据此回退到 legacy 。同理 5xx 是服务端故障,也不是时代证据。
- 只支持现代版的服务端,应在返回给
initialize的错误里列出支持版本——老客户端没有前推机制,这可能是用户能看到的唯一诊断信息。 - 会话状态要显式化:新版不再有传输层会话,如果业务需要跨调用状态,应由工具自己签发一个 handle,让模型作为参数传回来。这样做的好处是模型能”看见”这个句柄,比藏在传输层里更可控。
兼容性矩阵:谁连谁会怎样
- Modern 客户端 → Modern 服务端:正常。
server/discover可选,版本不匹配时按错误重试。 - Modern 客户端 → Legacy 服务端:失败。服务端可能拒绝、静默,甚至用 legacy 语义处理同名方法。 stdio 上建议先发
server/discover以确定性失败。 - Dual-era → Modern:正常,探测返回 DiscoverResult 。
- Legacy 客户端 → Modern 服务端:失败,且老客户端无法前推——这就是为什么现代服务端要在错误里写清支持的版本。
实践建议:如果你是服务端作者且需要照顾存量客户端,做 dual-era 支持;如果是全新项目,直接 pin 现代版本,省掉一整套兼容分支。
和旧版实现怎么共存
传输层的选择会影响探测方式:MCP 传输方式 stdio 与 Streamable HTTP 的差异在这里很关键——stdio 上用 server/discover 探测,失败即回退;Streamable HTTP 上先发现代请求,再检查 400 响应的 body 判断要不要回退。
错误处理层面,MCP 错误处理 区分了协议错误与工具执行错误两类,UnsupportedProtocolVersionError 属于前者,必须在传输层就处理掉,不能冒泡到业务逻辑。验证实现是否正确,用 MCP Inspector 与协议一致性测试 是最快的方式;如果你在写客户端,可以参考 Python MCP 客户端开发 的骨架。










评论 (0)