双纪元 wire codec(v2 的灵魂)
本章讲 v2 最有工程含量的设计:同一套 TypeScript 类型怎么同时服务两套不兼容的协议线格式(2025 纪元和 2026 纪元)。读懂这一章,你就懂了 v2 为什么这样分包、为什么
core-internal是私有的。
1. 它要解决的小问题
MCP 协议在演进:2026-07-28 这版改了线格式——每个请求要带 _meta 信封、结果要带 resultType 判别字段、删掉了 tasks/* 和 initialize 等一批方法、服务器不能再反向发请求。
问题:怎么让一份 SDK 同时正确说两种话,又不让业务代码(你的工具处理器)感知到差异?
直接的做法是到处 if (version >= '2026')——但那会让纪元判断散 落在几十个文件里,无法维护。v2 的答案是:把所有纪元差异收口到两个 codec 对象里。
2. 思路/直觉:中性类型 + 纪元 codec
核心分层(wire/codec.ts:1-43 的模块注释把这讲得很透):
┌─────────────────────────────────────────────┐
│ 公共类型层(中性) │ ← 你和处理器看到的
│ 没有 resultType、没有 _meta 信封、没有重试字段 │
└───────────────────┬─────────────────────────┘
│ encode(中性 → 线)
│ decode(线 → 中性)
┌────────────┴────────────┐
▼ ▼
┌──────────────┐ ┌──────────────┐
│ rev2025Codec │ │ rev2026Codec │ ← 各自拥有 registry + schema + 编解码
│ (legacy) │ │ (modern) │
└──────────────┘ └──────────────┘
一句话直觉:类型层是「世界语」,codec 是「翻译官」。 处理器永远说世界语;codec 在出入口把世界语翻成 2025 方言或 2026 方言。选哪个翻译官,由「协商出的协议版本」唯一决定。
3. 核心机制
3.1 选 codec:codecForVersion,多对一
纪元解析是一个纯函数,而且是多对一的:
// packages/core-internal/src/wire/codec.ts:298-300
export function codecForVersion(version: string | undefined): WireCodec {
return version !== undefined && isModernProtocolVersion(version) ? rev2026Codec : rev2025Codec;
}
关键认知(注释 codec.ts:16-23 的 REQUIRED DISCLOSURE):5 个 legacy 版本(2024-10-07 … 2025-11-25)共享同一套线格式,全部映射到唯一的 2025-era codec。 codec 是按「纪元」分的,不是按「精确版本」分的——只有当线格式真的分叉时才会有新 codec。undefined 或未知版本默认走 2025 codec(protocolEras.ts:isModernProtocolVersion 的判定是简单的字符串比较 version >= '2026-07-28',因为 ISO 日期字典序即时间序)。
WireEra 只有两个值(codec.ts:66):
export type WireEra = '2025-11-25' | '2026-07-28';
3.2 「删除即物理删除」:registry membership
这是整个设计最精彩的不变量。2026 纪元删掉了一批方法(tasks/*、initialize、ping、logging/setLevel、resources/(un)subscribe、所有服务器→客户端的请求)。SDK 怎么强制「这些方法在 2026 连接上不存在」?
靠 registry 成员资格,而不是靠一张黑名单。 每个 codec 有 hasRequestMethod / hasNotificationMethod(codec.ts:163-165)。判断「某方法是不是属于某纪元」就是查它在不在该 codec 的 registry 里。
效果(codec.ts:24-31 注释):
| 方向 | 方法属于 spec 但不在本纪元 registry | 结果 |
|---|---|---|
| 入站 | spec 方法缺席本纪元 | −32601 Method not found 凭缺席,在查处理器之前 |
| 出站 | 想发本纪元没有的 spec 方法 | 本地抛 SdkError,字节根本不上线 |
这就是第 2 章流水线里的「纪元门(④)」。看入站门的真实判断:
// packages/core-internal/src/shared/protocol.ts:960-963 —— 入站纪元门
if (isSpecRequestMethod(request.method) && !codec.hasRequestMethod(request.method)) {
sendErrorResponse(ProtocolErrorCode.MethodNotFound, 'Method not found');
return; // 即使你为 tasks/get 装了自定义处理器,在 2026 连接上它也被拦在这里
}
自定义处理器不能「复活」被删的 spec 方法: 若一个方法属于「spec 方法宇宙」(所有 codec registry 的并集,见下),它永远被纪元门管制。你给
tasks/get装的自定义处理器,只在定义它的那个纪元上生效。而完全在 spec 宇宙之外的方法(你自己的扩展方法)则纪元无关、需要显式 schema(codec.ts:33-38)。
3.3 「spec 方法宇宙」是派生的,不是手写的
怎么知道一个方法算不算 spec 方法?把所有 codec 的 registry 求并集:
// packages/core-internal/src/wire/codec.ts:322-330
export function isSpecRequestMethod(method: string): boolean {
return ALL_CODECS.some(codec => codec.hasRequestMethod(method));
}
const ALL_CODECS: readonly WireCodec[] = [rev2025Codec, rev2026Codec];
注释点破设计意图(codec.ts:315-321):这个并集是派生的,绝不手工维护。「以前那 种 LEGACY_ONLY_METHODS 硬编码表,正是 registry 成员资格要取代的东西。」
3.4 codec 的契约:一组纯函数
WireCodec 接口(codec.ts:159-287)是「函数式」的——调用方永远看不到 Zod schema,只看到三态校验结果 ValidateOutcome:
// packages/core-internal/src/wire/codec.ts:101-113 —— 三态:这个区分是关键
type ValidateOutcome<T> =
| { ok: true; value: T }
| { ok: false; reason: 'not-in-era' } // 方法是 spec 但不在本纪元 → −32601
| { ok: false; reason: 'invalid'; message: string }; // 方法在,但负载不合 schema → −32602
为什么坚持三态而不是简单的 ok/fail?注释(codec.ts:93-100)说:把「不在本纪元」塌缩成「invalid」会破坏多轮交互的带内回退链——回退逻辑靠 not-in-era 信号继续往下试 validateInputRequest,若当成失败就永远不回退了。
codec 还owns几个纪元差异点:
| 契约方法 | 干什么 | 纪元差异 |
|---|---|---|
decodeResult | 解码前先做 raw resultType 判别 | 2026:必须有判别字段;2025:resultType 是外来词,抬升时剥掉 |
encodeResult | 中性结果 → 线形态(stamp 缝) | 2025:恒等(永不 stamp);2026:stamp resultType、删字段、填 ttlMs/cacheScope |
outboundEnvelope | 造出站 _meta 信封 | 2025:返回 undefined(legacy 字节逐字节不变);2026:塞协议版本/客户端信息等 |
projectCallToolResult | tools/call 结果投影 | 任意纪元:TextContent 自动追加;仅 2025:{result:…} 包裹非对象值 |
encodeErrorCode | 中性错误码 → 线错误码 | 目前两纪元一致(如 −32002 → −32602),但留了分叉的位置 |
3.5 「永不 stamp」保证:legacy 字节逐字节不变
一个反复出现的承诺:2025-era codec 在所有可能改字节的地方都是恒等的。encodeResult 2025 路径没有任何 stamp 代码(codec.ts:255-263);outboundEnvelope 返回 undefined 让出站消息按引用原样返回(protocol.ts:634-644 的 _envelopeOutbound)。
好处:升级到 v2 后,对一个还在说 2025 的旧客户端,你发出去的字节和没有这套机制的旧 SDK 一模一样——零回归风险。
4. 主线:server/discover 之前的「引导窗口」
有个鸡生蛋问题:协商还没完成时,initialize(legacy)和 server/discover(modern)这两个握手消息本身该用哪个 codec?
答案:按方法名引导固定(bootstrap pin)。 在协商版本仍是 undefined 时,这两个握手方法「自报家门」——initialize 就是 legacy 握手,server/discover 就是 modern 探测:
// packages/core-internal/src/shared/protocol.ts:1265-1271 —— 出站 codec 解析
private _resolveOutboundCodec(method: string): WireCodec {
if (this._negotiatedProtocolVersion === undefined) {
const pinned = bootstrapOutboundCodec(method); // 协商窗口:按方法名引导
if (pinned) return pinned;
}
return this._negotiatedWireCodec(); // 协商后:实例纪元说了算
}
一旦协商出版本,实例纪元就是唯一权威——已建立的连接绝不会把某个方法重新路由到另一个纪元。
5. 巧妙之处
-
纪元是「实例状态」,不是「每消息开关」: 协商出的版本存在
Protocol._negotiatedProtocolVersion字段里(protocol.ts:534),没有任何「副表」。codec 解析就是codecForVersion(<实例状态>)。传输边缘可能给消息打分类标签(classification),但那只用来校验消息纪元是否和实例纪元一致——不一致就是路由错误(回 −32022),绝不据此切换纪元(codec.ts:302-313)。 -
core-internal私有是有意的: 注释明令(codec.ts:39-43)wire/下一切都是私有的,任何 per-revision 的东西绝不能从core/public导出。公共 API 只暴露中性类型;纪元机密永不外泄。
6. 边界与局限
- 目前只有两个 codec。新增纪元意味着新增一个 codec 对象并加进
ALL_CODECS——isSpecRequestMethod等派生函数会自动跟上。 MODERN_WIRE_REVISION = '2026-07-28'是内部常量,故意不做成公共常量(codec.ts:73)——在「纪元感知的列表语义」就绪前,不让公共的 modern 版本常量泄出去。- 中性类型层为了「中性」付出了代价:像
sampling/createMessage这种结果 schema 依赖请求参数(有无 tools)的方法,需要专门的samplingResultVariant(codec.ts:204-206)而非普通的 registry 查表。
7. 横向对比
多数协议 SDK 用「版本号 + 一堆条件分支」处理协议演进。MCP v2 把演进做成了类型不变量:删除是物理的(registry 缺席)、legacy 是字节恒等的(永不 stamp)、纪元是实例状态(无副表)。代价是分包更复杂(私有 core-internal + 两个 codec),收益是「业务代码完全纪元无关」。这与 02-protocol-and-transport.md 的入站流水线、04-client-and-negotiation.md 的连接期协商共同构成 v2 的三大支柱。
8. 代码地图
| 主题 | 文件 | 符号 |
|---|---|---|
| codec 契约 | packages/core-internal/src/wire/codec.ts | WireCodec、ValidateOutcome、DecodedResult |
| 选 codec | packages/core-internal/src/wire/codec.ts | codecForVersion、classifiedWireEra |
| spec 方法宇宙 | packages/core-internal/src/wire/codec.ts | isSpecRequestMethod、ALL_CODECS |
| 2025 codec | packages/core-internal/src/wire/rev2025-11-25/codec.ts | rev2025Codec |
| 2026 codec | packages/core-internal/src/wire/rev2026-07-28/codec.ts | rev2026Codec |
| 纪元判定 | packages/core-internal/src/shared/protocolEras.ts | isModernProtocolVersion、FIRST_MODERN_PROTOCOL_VERSION |
| 引导固定 | packages/core-internal/src/wire/bootstrap.ts | bootstrapOutboundCodec |
| 出站纪元门 | packages/core-internal/src/shared/protocol.ts | _assertOutboundRequestInEra、_resolveOutboundCodec |