Model Context Protocol TypeScript SDK 协议时代(Protocol Era)深度指南:从 versionNegotiation 到双时代服务 Model Context Protocol TypeScript SDK 协议时代Protocol Era深度指南从 versionNegotiation 到双时代服务【免费下载链接】typescript-sdkThe official TypeScript SDK for Model Context Protocol servers and clients项目地址: https://gitcode.com/GitHub_Trending/ty/typescript-sdk本文围绕modelcontextprotocol/typescript-sdkMCP TypeScript SDK中docs/protocol-versions.md定义的核心概念——**协议时代era**展开系统讲解legacy2024-10-07 至 2025-11-25与modern2026-07-28两套线行为族的差异、客户端如何通过versionNegotiation自动协商/固定时代、服务端如何用createMcpHandler与serveStdio在同一入口同时服务两个时代。读完本文你将能独立配置客户端探测与回退策略、利用缓存判定跳过探测、读懂探测失败的各种类型化错误并基于源码理解两条时代路径的底层实现原理。两个时代行为族而非版本字符串era时代是一个行为族behavior family不是某个版本字符串。这是理解 MCP 协议演进的关键前提。从2024-10-07到2025-11-25的所有协议修订版本都以initialize握手开场共享同一条线上wire行为——SDK 将这一族称为legacy旧时代。2026-07-28修订版本开启了modern新时代不再有initialize取而代之的是server/discover通告并且每个请求都携带_meta信封envelope。SDK 的同一套Client类可以同时说两个时代同一批服务入口可以同时服务两个时代。一条连接的所属时代在连接建立的那一刻被决定一次其后所有差异都由协议时代对比表统一定义见下文「对比两个时代」。从源码看时代判定的落点非常明确客户端侧由 getProtocolEra() 报告——它在连接建立后根据已协商的协议版本调用isModernProtocolVersion得出modern | legacy连接前返回undefined服务端侧则由createMcpHandler/serveStdio的工厂上下文McpRequestContext.era向业务代码显式暴露定义见 createMcpHandler.ts 中的 McpRequestContext。从客户端协商时代versionNegotiationversionNegotiation选项决定connect()执行哪种握手。mode: auto会先用server/discover探测服务器然后按探测结果在对应时代上连接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());上例中的http://localhost:3000/mcp是一个由createMcpHandler构建的服务器后文「一个入口服务两个时代」会演示其构建方式因此探测会找到 2026-07-28 时代输出modern把同样的选项指向一个仅支持 2025 的服务器connect()会回退到initialize握手——只多一次往返不会报错const fallback new Client({ name: my-client, version: 1.0.0 }, { versionNegotiation: { mode: auto } }); await fallback.connect(new StreamableHTTPClientTransport(new URL(http://localhost:4000/mcp))); console.log(fallback.getProtocolEra());输出legacygetProtocolEra()报告连接最终落定的时代在connect()resolve 之前返回undefined之后永不改变。以上代码段全部取自 examples/guides/protocolVersions.examples.ts 中可真实运行的示例。该文件的注释揭示了测试骨架示例运行器在进程内拦截globalThis.fetch把localhost:3000路由到双时代createMcpHandler入口、把localhost:4000路由到legacyStatelessFallback构建的纯 2025 服务器——所以文档中引用的探测、回退与固定时代拒绝行为都是真实执行过的不是纸面描述。固定一个时代pin 模式永不回退mode共有三个取值第一个是默认值缺省或mode: legacy——2025 版initialize握手逐字节不变不探测。mode: auto——先用server/discover探测对纯 2025 服务器回退到initialize。mode: { pin: 2026-07-28 }——只要该修订版本否则拒绝。pin 模式永不回退。对同一个纯 2025 服务器使用 pin 模式connect()会直接 reject 而不是回退import { SdkError } from modelcontextprotocol/client; const pinned new Client({ name: my-client, version: 1.0.0 }, { versionNegotiation: { mode: { pin: 2026-07-28 } } }); try { await pinned.connect(new StreamableHTTPClientTransport(new URL(http://localhost:4000/mcp))); } catch (error) { if (error instanceof SdkError) console.log(${error.code}: ${error.message}); }这个拒绝是本地的类型化SdkError——除了探测之外没有任何东西到达服务器ERA_NEGOTIATION_FAILED: Version negotiation failed: the server did not offer pinned protocol version 2026-07-28 via server/discover (no fallback in pin mode)从 versionNegotiation.ts 的 resolveVersionNegotiation 可以看到pin 目标在解析阶段就受到约束pin只能指向 2026-07-28 及以后的现代修订版本isModernProtocolVersion校验若传入 2025 时代版本会直接抛出TypeError并提示改用mode: legacy或省略versionNegotiation。这从类型层保证了「pin 只认现代版本」的语义不会在使用阶段出错。用缓存判定跳过探测mode: auto在每次全新连接时都要付出一次探测的代价。如果宿主已经知道服务器的时代——例如来自注册表条目或来自先前某次连接的结果——可以通过ConnectOptions.prior导出类型PriorDiscovery跳过它{ kind: modern, discover }——直接采纳先前取得的DiscoverResult零往返{ kind: legacy }——直接走initialize握手不探测。新鲜度freshness是宿主的职责不是 SDK 的职责过期的 modern 判定会在第一个请求处响亮失败而过期的 legacy 判定会在一台已升级的服务器上静默成功升级后的服务器仍然会应答initialize。因此请在你的自有存储中为缓存的 legacy 判定打上日期超过你自己的策略时限后就停止提供它。完整的循环包括重新探测并回填缓存的 re-probe 流程见 缓存发现判定Caching discovery verdicts。PriorDiscovery类型的定义与新鲜度警告注释位于 probeClassifier.tsConnectOptions.prior的字段文档见 client.ts。与之配套的还有两个读取 APIgetDiscoverResult()返回最近一次现代探测/采纳得到的DiscoverResult可直接JSON.stringify持久化后回喂给prior而validatePrior会在采纳前校验持久化 blob 的合法性——不认识的kind、携带DiscoverResult形状成员的 legacy 判定、schema 不过的 moderndiscover负载都会被类型化SdkError拒绝杜绝「损坏的 blob 决定时代」这类静默事故见 client.ts validatePrior。理解探测probeprobe选项约束auto与 pin 模式在任何其他事情之前执行的server/discover往返const cli new Client( { name: my-client, version: 1.0.0 }, { versionNegotiation: { mode: auto, probe: { timeoutMs: 10_000, // default: the connections request timeout maxRetries: 0 // default: no probe re-sends after a timeout } } } );两个字段的语义在 VersionNegotiationProbeOptions 中有精确定义timeoutMs——探测交换的超时毫秒数默认继承连接的请求超时DEFAULT_REQUEST_TIMEOUT_MSEC或connect()传入的timeoutmaxRetries——超时后重发探测的次数默认0不重发。注意它只约束超时重发规范强制的-32022修正性续接select-and-continue双方协商出一个共同版本是独立的协商步骤永远不会计入maxRetries。探测超时是传输感知的在 stdio 上一个沉默的服务器就是 legacy 服务器——因此connect()回退到initialize在 HTTP 上沉默意味着宕机——因此connect()用SdkError(RequestTimeout)拒绝而不是把一台死服务器误报为 legacy。这一分支逻辑的权威实现位于 probeClassifier.ts 的 classifyProbeOutcometimeout与closed两种结果都会先检查transportKindstdio 一律判为legacy信号HTTP 则保留类型化错误。有一个浏览器特例探测期间出现的不透明 CORSTypeErroropaque fetch TypeError会被当作 legacy 信号回退——因为已部署的 2025 服务器通常带有早于 2026 头部的 CORS 允许列表而 legacy 回退路径只携带这些旧列表已覆盖的请求头可以照常通过预检见 classifyNetworkError。认证状态不是时代证据HTTP 的401/403拒绝探测时呈现为类型化的认证失败绝不会触发 legacy 回退也绝不会报EraNegotiationFailed——因此以该错误码为键的时代恢复流程如 网关指南 中的配方不可能被认证墙错误消费。具体规则普通的401/403无论是否配置了authProvider都会以携带状态的SdkHttpError拒绝connect()401→ClientHttpAuthentication403→ClientHttpForbidden有一种 403 形状不同带errorinsufficient_scope的WWW-Authenticate质询会进入 Streamable HTTP 传输的 step-up 流程无论有无 provider无 provider 时以该流程的类型化InsufficientScopeError拒绝有 provider 时401以及403 insufficient_scope质询会先跑认证流程逃逸出来的错误原样抛出、身份保持完整——传输在认证接缝token()读取、onUnauthorized含自定义回调、step-up处给错误盖章所以finishAuth()的UnauthorizedError、流程的类型化失败OAuthError、InsufficientScopeError、重新认证后的 401 诊断乃至回调内部未类型化的崩溃都会原样传播5xx拒绝探测是服务器故障而非时代证据connect()以SdkHttpError(EraNegotiationFailed)拒绝并指明状态码。这些按状态分类的行读取的是传输的类型化 HTTP 拒绝SDK 的 Streamable HTTP 传输在拒绝时携带状态码而 legacy SSE 客户端传输会把非 2xx 的 POST 报告为通用错误因此在 SSE 上探测被拒呈现为通用的Version negotiation probe failed连接错误。认证先于时代落定401永远不会决定时代——认证墙在 MCP 层看到server/discover之前就回答了认证后的 re-probe 才提供真正的时代证据。上述分类逻辑完整实现了 classifyHttpError 与auth-required分支中。stdio 上的兄弟进程探测在 SDK 自带的 stdio 传输上恰好是StdioClientTransport基类本身子类如自定义 stdio 形状的传输是在原位探测探测运行在一个由相同参数派生的**短生命周期兄弟进程sibling process**上。原因很实际部分 stdio 服务器会在任何 pre-initialize请求上直接退出官方 Rust SDK、rmcp 等构建的服务器就是这样所以探测绝不能消耗调用方唯一的子进程。兄弟进程是隐形基础设施它的 stderr 被丢弃时代确定后即被回收调用方的传输只在之后恰好派生一次且其线路上永远不会携带server/discover。一个在探测时退出的子进程就只是一个 legacy 服务器其退出必须关闭子进程的 stdio 管道才能被识别——若被一个持有管道不放的辅助进程掩盖则落入探测超时路径。探测期间关闭调用方的传输会以类型化SdkError(EraNegotiationFailed)中止connect()且会话子进程永远不会被派生。在 HTTP 上以及原位探测的自定义 stdio 形状传输上探测中途连接关闭与任何探测传输故障一样以同一个类型化错误拒绝。这段逻辑的实现位于 versionNegotiation.ts 的 negotiateStdioViaSibling兄弟传输用同一构造函数 保留的_serverParams重建stderr: ignore探测期间对会话传输的close()打桩以竞速中止探测随后disposeSibling尽力回收信号升级等待进程退出持有管道不放的辅助进程不会阻塞回收最后才把close恢复原样。注意 readStdioServerParams 的判定只有基类原型自带_dispose回收器才走兄弟进程路径——子类无法用保留参数忠实地重建所以子类一律原位探测。supportedProtocolVersions 如何塑造探测客户端的supportedProtocolVersions选项会塑造探测其中 2026 的条目就是探测提供的版本而 legacy 回退只在列表保留 pre-2026 条目时可用。列表不含任何 pre-2026 条目会移除回退——此时对纯 2025 服务器connect()以SdkError(EraNegotiationFailed)拒绝见 versionNegotiation.ts 的 modern-only 分支 与 probeClassifier.ts 的 fallbackAvailable 说明。一个警告CLI 工具不要默认 auto不要把按次派生进程的 CLI 工具默认设为auto。在 stdio 上一个从不应答未知 pre-initialize请求的 legacy 服务器会让connect()在完整探测超时后才回退而且每次连接都会多派生一个短命服务器进程。请保留默认值并把auto或 pin暴露为命令行标志由用户显式开启。一个入口服务两个时代createMcpHandler就是上文服务了两个客户端的 HTTP 入口它每个请求构建一个全新的服务器实例并把该请求所属的era传给工厂import { createMcpHandler, McpServer } from modelcontextprotocol/server; import * as z from zod/v4; const handler createMcpHandler(({ era }) { const server new McpServer({ name: forecast, version: 1.0.0 }); server.registerTool( forecast, { description: Forecast for a city, inputSchema: z.object({ city: z.string() }) }, async ({ city }) ({ content: [{ type: text, text: ${city}: sunny (${era} era) }] }) ); return server; });默认情况下该入口也会按请求服务 2025 时代流量legacy: stateless传legacy: reject则拒绝之。再连接一个使用默认模式的客户端到同一 URL——不探测走 2025 握手——然后从两个客户端分别调用工具const defaultClient new Client({ name: my-client, version: 1.0.0 }); await defaultClient.connect(new StreamableHTTPClientTransport(new URL(http://localhost:3000/mcp))); for (const caller of [client, defaultClient]) { const result await caller.callTool({ name: forecast, arguments: { city: Berlin } }); console.log(caller.getProtocolEra(), JSON.stringify(result.content)); }一个端点、一个工厂、两个时代——并且时代到达了处理器modern [{type:text,text:Berlin: sunny (modern era)}] legacy [{type:text,text:Berlin: sunny (legacy era)}]从源码看这条双时代路由的完整链路位于 createMcpHandler.ts入口对每个入站 HTTP 请求做恰好一次分类body 优先classifyInboundRequest据此路由携带_meta信封的请求走现代路径工厂构建新实例、按声明修订版本标记、接入单次交换的 per-request 传输无信封声明的请求含initialize、GET/DELETE 会话操作、2025 通知 POST是 legacy 流量legacy: stateless默认通过 createLegacyStatelessFallback 实现每个 POST 由工厂产出的新实例 仅以sessionIdGenerator: undefined构造的 streamable HTTP 传输服务既有的无状态惯用法GET/DELETE 应答405legacy: reject是现代专用严格模式legacy 分类请求以不支持协议版本错误拒绝legacy 分类的通知以202应答并丢弃此模式下没有 2025 服务值得一提的设计约束不存在「传 handler 给 legacy 选项」的用法——legacy只接受stateless | reject两种姿态传入函数会直接抛TypeError。要保留既有 legacy 部署例如有会话的 streamable HTTP 接线用导出的 isLegacyRequest 谓词在用户层分流——它复用的是入口自身的分类代码绝不会与入口的路由决策产生分歧。stdio 侧serveStdio在 stdio 上modelcontextprotocol/server/stdio的serveStdio(factory)是同一形状的按连接服务开场交换钉死连接的所属时代legacy: reject拒绝 2025 开场。详见 serveStdio 入口文档开场交换的分类规则与 HTTP 入口完全同源同一套 body 优先规则——initialize或任何无声明消息开场即 2025 会话携带有效现代信封的开场钉死现代server/discover探测先由乐观构建的现代实例应答但不钉死允许客户端随后回退到initialize届时探测实例被丢弃、新 legacy 实例服务握手现代时代钉死后再来的无声明initialize以不支持协议版本错误拒绝规范禁止在确认现代后回退。legacy选项的完整语义与两个入口的托管配方见 服务 legacy 客户端Serve legacy clients。对比两个时代下表是这些文档中唯一一份时代差异表。客户端的getProtocolEra()和服务端工厂的era会告诉你当前处于哪一列。Axis维度2025 时代legacy2024-10-07…2025-11-252026 时代modern2026-07-28服务端 HTTP 入口*StreamableHTTPServerTransportcreateMcpHandlerlegacy: stateless同时服务 2025服务端 stdio 入口server.connect(new StdioServerTransport())serveStdio(factory)默认也服务 2025除非legacy: reject客户端连接方式initialize握手server/discover探测versionNegotiation服务端看到的客户端身份getClientCapabilities()/getClientVersion()initialize 作用域ctx.mcpReq.envelope每个请求携带服务端→客户端请求ctx.mcpReq.elicitInput/requestSampling、实例方法createMessage()从处理器return inputRequired(...)变更通知非请求式list_changed/resources/updatedsubscriptions/listen流客户端取消Streamable HTTPPOSTnotifications/cancelled关闭该请求的 SSE 响应流ctx.mcpReq.log()级别过滤会话级logging/setLevel每请求logLevel_meta信封键缺省 无日志携带 JSON-RPC 错误体的 HTTP400SdkHttpErrorProtocolError带内交付时代不匹配的规范方法出站不适用SdkError(MethodNotSupportedByProtocolVersion)存活检查client.ping()未定义——出站调用按上一条时代不匹配行拒绝区分「弃用」与「时代」弃用deprecation不是时代差异。sampling、roots以及ctx.mcpReq.log()背后的logging能力自2026-07-28起被弃用SEP-2577但至少十二个月内仍保留在规范中某一特定连接上由哪个 API 承载它们是时代差异——表中已有对应行。每个被弃用的表面都单独开页并带有指明迁移目标的 sunset 横幅弃用落地时矩阵中的任何行都不会移动。在此处链接而不是内联解释时代差异只存在于本页docs/protocol-versions.md别无分号。本文档体系中的每一页对时代最多花一句话解释然后链接回此处你自己的服务器文档也应如此——例如「结构化结果的线上编码因协议时代而异——参见 Protocol versions」。要点回顾时代是行为族legacy覆盖2024-10-07至2025-11-25modern始于2026-07-28。versionNegotiation决定客户端握手默认是逐字节不变的 2025initialize不探测。mode: auto用server/discover探测并回退到initializepin 模式永不回退以SdkError(EraNegotiationFailed)拒绝。getProtocolEra()在客户端报告协商出的时代createMcpHandler/serveStdio工厂接收即将服务的era。本页的行为矩阵是唯一一份其他页面只用一行链接回这里。弃用SEP-2577不是时代差异。延伸阅读想实际跑通本文所有代码可阅读 examples/guides/protocolVersions.examples.ts文档中每个 ts 代码块都由该文件的//#region同步而来且可在examples/目录下用npx tsx guides/protocolVersions.examples.ts直接执行想深入客户端探测引擎可精读 versionNegotiation.ts 与 probeClassifier.ts想了解探测/时代在网关与缓存场景的完整闭环见 网关指南。【免费下载链接】typescript-sdkThe official TypeScript SDK for Model Context Protocol servers and clients项目地址: https://gitcode.com/GitHub_Trending/ty/typescript-sdk创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考