场景代入:你正在调试一个 MCP 服务,突然发现某个主流 AI 客户端连不上你的服务器,报错信息模糊,只有 HTTP 400。你翻遍日志,发现是版本号不匹配——客户端发来的版本比你服务器支持的版本新那么一点点,而你的服务器直接拒绝了。这种事听起来很蠢,但真实发生在 2026 年 9 月 24 日,一个开发者自己公司的 MCP 服务器上。MCP(Model Context Protocol,模型上下文协议)是连接 AI 应用与外部工具/数据源的开放标准,简单说就是让 ChatGPT 这类助手能调用你的 API 的标准接口。版本号是一个日期字符串,比如 2025-11-25,HTTP 客户端(也就是 AI 应用)每次请求都会带上这个 header。服务器根据自己代码里声明的支持版本列表(一个硬编码的数组)来决定是否接受请求。
问题出在哪?这个服务器只声明支持 2025-06-18 一个版本,而那天主流的 AI 客户端发送的版本是 2025-11-25。服务器检查后发现不在列表里,就在认证之前直接返回 400,导致整个连接失败。作者记录了自己怎么修复这个过程,并深入分析了背后的协议规范矛盾和鲁棒性原则。
第一次修复很自然:AI 代理(也就是自动修复工具)只把观察到的 2025-11-25 加进了支持列表。但作者立刻指出:AI 客户端更新频繁,下次它发个 2025-12-01,你又得手动加。这种按观察到的版本逐个打补丁的做法,本质上是把协议版本检查变成了一个不断维护的黑名单问题。第二次修复就聪明多了:支持列表改成两个(2025-11-25 和 2025-06-18),同时加了一条规则——只要是日期格式且小于 2026-07-28 的值,即使不在列表里也接受。这条规则不是随意定的,因为 MCP 规范在 2026-07-28 版本发生了一个根本变化:从“客户端先发 initialize 请求协商版本”变成了“直接在每个请求的 _meta 字段里声明版本”。前者被称为 legacy 版本(旧版本),后者是 modern 版本(新版本)。

这里有个关键的矛盾点。MCP 规范有一条 MUST 要求(强制项):服务器收到不支持的版本 header 时,必须返回 400。如果严格照做,那么任何不在列表里的版本都得拒。但作者选择的解法是:对 2026-07-28 之前的日期值(即便不在列表)也接受,这显然违反了 MUST。为什么敢这样?作者引用 RFC 9413 中的鲁棒性原则(robustness principle,即“对输入宽容,对输出严格”)和它的限制。原版的鲁棒性原则鼓励容忍意外输入,但 RFC 9413 对它做了约束:不能无限制地宽容,必须是在有明确规则的前提下扩展。作者认为第二次修复既不算是“容忍意外输入”,也不算是 RFC 9413 2.2 节定义的“明确规则下的扩展”,而是一种“对规范明确定义的行为,故意选择不同处理方式”的实现。但这样做值得吗?
作者在文中用表格详细展示了修复前后的检查结果,并且对比了规范的两条 MUST(另一条是:收到现代版本且不在支持范围时也要 400?其实这里规范要求的是如果客户端用现代版本,服务器应该处理,但作者保留了 400 给 >=2026-07-28 的日期)。在修复后的第二天(2026-09-25),AI 代理又给服务器加了响应 2026-07-28 版本的代码,也就是说最终服务器能同时处理新旧两种协议握手。这也解释了为什么第一天只处理旧版本——因为问题源于旧客户端发来的旧版本号,但作者发现必须兼容未来。

关于版本检查的硬核细节,作者用表格对比了不同 header 值在修复前后的表现。修复前,只有 2025-06-18 被接受;修复后,2025-11-25 也被接受,所有小于 2026-07-28 的日期格式值都被接受(哪怕不在列表)。而对于 2026-07-28 及之后的日期,以及非日期格式的值,仍然返回 400。这个设计背后有一个很实际的考量:MCP 规范在 2026-07-28 之后取消了 initialize 请求,如果服务器收到现代版本但自己还只能处理旧握手,那必须拒绝,否则会进入一个无法协商的状态。

作者还记录了一个小细节:在修复前,服务器代码里有个注释,解释了为什么不声明 2025-03-26 版本——那个版本要求实现接受 JSON-RPC 批量请求,而该服务器不支持。这解释了为什么支持列表里没有那个版本。这种“因为某个特性没实现所以不声明该版本”的做法是合理的,但问题在于:MCP 的版本机制是“客户端发一个它想用的版本,服务器必须明确同意”,如果服务器只支持一个老版本,而客户端总想用最新,冲突就不可避免。作者的经验是:当你在做一个 MCP 服务器时,版本检查不应该做成黑名单(只允许列表里的),而应该做成白名单加宽容规则(接受所有符合日期格式的旧版本,只拒绝严格不认识的格式)。

最让我意外的是,作者用了“鲁棒性原则”这个来自互联网传输协议的老原则来为自己的设计辩护。虽然 RFC 9413 明确说“宽容不能盲目”,但作者认为在这种场景下,对一个版本日期字符串的宽容是低风险的——因为协议版本之间的差异通常是渐进式的,而且客户端发过来的版本号里包含了“我理解并期望的协议行为”,服务器如果接受一个稍微老一点的版本,最多是有些新特性不可用,不会导致崩溃。而拒绝则直接切断所有连接,可用性归零。这非常符合“对输入宽容”的精神。但作者也提醒了一个坑:这种宽容必须限定在“日期格式且小于某个临界值”,不能无差别接受所有未知版本,否则遇到真正的恶意或不兼容请求时会出大问题。

对你有什么借鉴?如果你在开发任何提供出去服务的组件(不只是 MCP),版本协商是常见问题。第一,不要只根据“当前观察到的版本”打补丁,要设计一个基于语义的兼容区间。第二,当规范更新导致协议行为变化时,要明确区分“旧协议”和“新协议”的握手路径,服务器可以同时支持两者,用版本号作为分流开关。第三,对于协议里的 MUST 要求,要理解它的意图——如果盲目遵守会导致可用性灾难,是否值得在文档里明确标注偏离(像作者那样分析 RFC)?最后,记录每个被拒版本和 User-Agent(客户端标识)到日志,当出现兼容问题时能快速定位是哪个客户端、什么版本。这篇实战记录的价值在于,它展示了一个真实的技术决策:在规范强制要求和实际可用性之间,工程师如何用原则(鲁棒性)来支撑自己的选择,而不是简单服从。下次你遇到协议版本不兼容时,这篇笔记能给你一个可复用的思考框架。
内容与图片版权归原作者所有 · 原文: https://dev.to/matsumotory/rejecting-unsupported-mcp-versions-left-an-ai-client-unable-to-connect-3jhh