🔌 第1章 快速了解MCP
📄 一句话概括

这条协议只有两年多的历史,却已经改了三次。 版本号就是日期,说的是「我按那一天那份文本说话」,不是软件版本。 一次连接里只有握手那一次机会说清自己会哪一版,之后全都不带版本号。

📝 本章和前面几章的关系

上一节看到四行报文,其中第一行里有个 protocolVersion 字段,当时没展开。 这一节专门讲它:为什么是日期、为什么要协商、谈不拢会怎样。 第 6 章那个自己写的 Server 要先把这一行处理对,理由也在这里。

一、它从哪儿来

这套东西最早不是标准,是一家公司给自己的编辑器写的内部约定。 编辑器要接一堆外部能力——查文件、查数据库、查工单——每接一个都要写一层胶水。 写法跟上一节那 24 份适配一样,只是发生在同一家公司内部,所以痛得更集中,也更容易被抽象掉。 抽象的结果就是:把「能力」写成一个独立进程,用一套固定的报文去问它。 编辑器不再关心能力怎么实现,只关心那张工具表。

一旦约定跟具体编辑器解绑,它就有机会变成公共标准。 这是它后来能扩散出去的前提。

二、版本号就是日期

2024-11-05、2025-03-26、2025-06-18,三个版本号长得像日期,因为它们就是日期。 这不是偷懒。 软件版本号(1.2.3)暗示「功能在增加」,而协议要表达的是「文本在第几天改过」, 用日期最不容易起歧义。 所以客户端说「我用 2024-11-05」,意思是「我按那一天那份文本说话」,而不是「我版本比较老」。

三次修订各加了什么,演示里那张表列了。 真正要记的只有一条:2025 年 3 月那次把长连接 SSE 换掉了,这件事第 7 章还要再讲一遍。

三、为什么要协商,而不是直接用最新版

因为服务端和客户端不是一起升级的。你手上的客户端可能半年前装的,连的 Server 是昨天刚更新的。 握手的规矩是:客户端报一个自己最想用的版本,再带上自己支持的全部版本; 服务端从里面挑一个自己也会的,回给客户端。 演示里客户端报 2025-06-18,服务端只会 2024-11-05,它没有顺着客户端说,而是老老实实回自己会的那个。

这是有意设计的:服务端不猜。 让步由客户端做——能降就降,不能降就断。 断掉比假装能用好。假装能用的结果是发出去的报文对方读不懂,报一个没人看得懂的错,排查三天。

四、为什么只在握手时说一次

版本号只出现在 initialize 那一次交换里。之后的 tools/list、tools/call 都不带。 这是省流量吗?不是。 是因为一次连接内的版本在握手时就定死了,后面每条报文再重复一遍,等于给「可能不一致」留了口子。 反过来推: Server 只有一次机会说清自己会说哪种话。 这一句解释了为什么第 6 章那个几十行的 Server 要先把 initialize 写对——写错一次,整条连接后面全废。

演示里第三段的输出特别值得看一眼:它回的那个版本号,跟你发的不一样。 第一次看到会以为出错,其实那就是协商成功的样子。

五、往后会变成什么

标题里写「未来」,但这一节不给预测。能给的是判断标准:一个接口标准能不能活下来,看三件事。 第一,实现成本够不够低。写一个 Server 要是一百行以内,小团队才愿意写; 要是要搭框架、注册一堆东西,就只有大厂写。 第二,有没有人靠它赚钱。目录站、托管平台、审核服务,这些生意一起来,标准就有人维护。

第三,上游会不会变卦。协议背后有公司,公司会调整方向。 所以别把业务逻辑写进协议细节里,写进 Server 里。 这三条对任何标准都成立。MCP 到目前为止三条都占着,但这不构成对未来的保证。

六、动手 Demo:三个版本,一次协商

先把三个版本的时间线和各自加了什么摆出来,再拿一个只会老版本的服务端试一遍。 最后一段是真的发了一次 initialize,看它怎么回。

SERVER = '2024-11-05'          # 这个服务端只会这一版


def handshake(client_wants, client_supports):
    if SERVER in client_supports:
        return SERVER, '接上'
    return None, '断开'


print(handshake('2024-11-05', ['2024-11-05', '2025-06-18']))
print(handshake('2025-06-18', ['2024-11-05', '2025-06-18']))
print(handshake('2025-06-18', ['2025-06-18']))

实跑输出:

--------------------------------------------------------------------
三个版本,一次协商
--------------------------------------------------------------------


  ① 时间线
  --------------------------------------------------------------------

| 版本号 | 时间 | 这一步加了什么 |
|---|---|---|
| 2024-11-05 | 2024-11-25 发布 | 首次公开:stdio 加 HTTP+SSE 两种传输 |
| 2025-03-26 | 2025-03-26 | Streamable HTTP 取代长连接 SSE,补 OAuth 2.1 |
| 2025-06-18 | 2025-06-18 | 工具可返回结构化结果,新增 elicitation |

  版本号就是日期。【它不是软件版本,是协议文本的修订日。】
  所以一次连接里说的「我用 2024-11-05」,意思是「我按那一天那份文本说话」。

  ② 服务端只认老版本时会怎样
  --------------------------------------------------------------------

| 客户端要的 | 客户端支持的 | 服务端回的 | 结果 |
|---|---|---|---|
| 2024-11-05 | 2024-11-05、2025-06-18 | 2024-11-05 | 接上 |
| 2025-06-18 | 2024-11-05、2025-06-18 | 2024-11-05 | 接上 |
| 2025-06-18 | 2025-06-18 | 2024-11-05 | 断开 |

  第二行是最常见的情况:新客户端连老服务端,【降级到老版本继续用】。
  第三行才是问题:客户端只会说新话,服务端只会说老话,只能断开。

  ③ 真发一次,看它回什么
  --------------------------------------------------------------------

客户端在 initialize 里报 2025-06-18,服务端的回答是:
    {
      "protocolVersion": "2024-11-05",
      "capabilities": {},
      "serverInfo": {
        "name": "trave-tools",
        "version": "0.1.0"
      }
    }
  注意它没有顺着客户端说,而是【回自己会的那个版本】。
  谁让步由客户端决定:能降就降,不能降就断。服务端不猜。

  ④ 版本写在哪儿
  --------------------------------------------------------------------
只在 initialize 那一次交换里出现。之后所有的 tools/list、tools/call 都不带版本号。
这就是为什么第 6 章那个几十行的 Server 要先把 initialize 处理对——
【它只有一次机会说清自己会说哪种话。】
  ! 协议还在往前走,本课取的是 2025-06-18 那份。以官方文档为准,别记日期。

①第一张表三行,第三行才是这半年真正加的东西。 前两行是「换传输」和「补鉴权」,第三行才开始动能力本身——工具能返回结构化结果、能反过来问用户。 ②第二张表三行,覆盖了实际会遇到的三种情况。 最常见的是第二行:新客户端连老服务端,降级继续用,用户完全无感。 ③第三行才是故障:客户端只会新话,服务端只会老话,只能断开。 这种断是好事,比连上以后每条报文都报错强得多。 ④第三段是真实输出。 注意服务端回的是自己的版本,不是客户端要的——很多人第一次看到这里以为程序写错了。 ⑤最后那句提醒:本课按 2025-06-18 那份写。日期会继续往前走,遇到对不上的地方,以官方文档为准。

30 秒实验

  1. 把演示里的 SERVER 改成 2025-06-18,三种情况的结果各是什么。哪一行变了,哪一行没变。
  2. 找一个你装过的、版本号长得像日期的软件。想一下它为什么不用 1.2.3。
  3. 如果你要自己写一个 Server,会在 initialize 里回哪个版本?为什么不能回「最新的那个」。

常见误区

  1. 「版本号越新越好,直接用最新的。」服务端回的是自己会的版本,不是你要的。 写死最新版,遇到老服务端就只有断开一条路。
  2. 「协商失败说明代码有 bug。」协商失败是正常结果,说明双方没有共同语言。这时候断开比硬连安全。
  3. 「版本号每条报文都要带。」只在 initialize 里出现一次。重复带,等于给「可能不一致」留口子。
自测三问(点开对答案) 1. 为什么版本号用日期而不用 1.2.3? —— 它要表达的是「文本在第几天改过」,不是「功能增加了多少」。日期最不容易起歧义。 2. 客户端报 2025-06-18,服务端只会 2024-11-05,会发生什么? —— 服务端回 2024-11-05。客户端的支持列表里有它,就降级继续用;没有就断开。 3. 为什么说服务端「只有一次机会」? —— 版本号只在 initialize 里交换一次,之后所有报文都不带。握手写错,整条连接全废。

版本的事说清了,还剩一个问题没答:这套约定到底省在哪儿。 下一节把「N 乘 M 变 N 加 M」算给你看,也顺手说清楚它跟 Function Calling 的关系。

相关笔记