MCP 协议的纯数据层:方法名、_meta 键、错误码、版本、能力、信封构造与解析。
零 I/O、零状态——传输在 mcp.transport,会话与回退策略在 mcp.client,
服务端派发在 mcp.server。本 ns 只回答「这条消息长什么样」。
规范 2026-07-28(SEP-2575 + SEP-2567)把 MCP 改成了无状态协议:
| legacy(≤ 2025-11-25) | modern(≥ 2026-07-28) | |
|---|---|---|
| 开场 | initialize 握手 + notifications/initialized | 没有握手,server/discover 可选 |
| 版本/能力/身份 | 握手时协商一次,之后靠会话记住 | 每条请求自带,放在 params._meta 里 |
| HTTP 会话 | Mcp-Session-Id | 没有会话;Mcp-Method / Mcp-Name 变必填头 |
| 结果 | 直接是 result 对象 | 多一个 resultType(complete / input_required) |
| roots / sampling / logging | 正常特性 | 全部 deprecated(SEP-2577) |
「服务器不得从上一条请求推断任何东西」是 modern 的核心不变量——所以
request 每次都把版本与能力塞进 _meta,这不是冗余,是协议要求。
modern 的 HTTP 传输要求 MCP-Protocol-Version 头与 _meta 里的版本一致,
不一致 server 回 -32020 HeaderMismatch。所以「往信封里写版本」和「往头里写
版本」必须是同一个值的两次投影,不能各写各的——transport 那侧因此从信封里
反读版本(protocol-version-of),而不是自己记一份。
MCP 协议的**纯数据层**:方法名、`_meta` 键、错误码、版本、能力、信封构造与解析。 **零 I/O、零状态**——传输在 `mcp.transport`,会话与回退策略在 `mcp.client`, 服务端派发在 `mcp.server`。本 ns 只回答「这条消息长什么样」。 ## 两个时代(本 ns 同时认识,因为编解码是共用的) 规范 `2026-07-28`(SEP-2575 + SEP-2567)把 MCP 改成了**无状态**协议: | | legacy(≤ 2025-11-25) | modern(≥ 2026-07-28) | |---|---|---| | 开场 | `initialize` 握手 + `notifications/initialized` | **没有握手**,`server/discover` 可选 | | 版本/能力/身份 | 握手时协商一次,之后靠会话记住 | **每条请求自带**,放在 `params._meta` 里 | | HTTP 会话 | `Mcp-Session-Id` | **没有会话**;`Mcp-Method` / `Mcp-Name` 变必填头 | | 结果 | 直接是 result 对象 | 多一个 `resultType`(`complete` / `input_required`) | | roots / sampling / logging | 正常特性 | **全部 deprecated**(SEP-2577) | 「服务器不得从上一条请求推断任何东西」是 modern 的核心不变量——所以 `request` 每次都把版本与能力塞进 `_meta`,这不是冗余,是协议要求。 ## 一处**必须**照做否则静默出错的地方 modern 的 HTTP 传输要求 `MCP-Protocol-Version` 头与 `_meta` 里的版本**一致**, 不一致 server 回 `-32020 HeaderMismatch`。所以「往信封里写版本」和「往头里写 版本」必须是同一个值的两次投影,不能各写各的——`transport` 那侧因此从信封里 反读版本(`protocol-version-of`),而不是自己记一份。
(best-legacy-version supported)从 server 报的 supported 列表里挑一个我们也支持的 legacy 版本;
挑不出就退到 2025-11-25(legacy 里最新的那个,initialize 本来就会再协商一次)。
从 server 报的 `supported` 列表里挑一个**我们也支持**的 legacy 版本; 挑不出就退到 `2025-11-25`(legacy 里最新的那个,`initialize` 本来就会再协商一次)。
(client-info)(client-info name version)Implementation:自报家门。协议明说它不做安全判断,只用于显示与排查。
`Implementation`:自报家门。协议明说它**不做安全判断**,只用于显示与排查。
我们作为 client 声明的能力。
只报真支持的:elicitation 我们能接(mcp.client 有回调挂点),
roots / sampling 是 2026-07-28 起 deprecated 的特性,不报。
extensions 里报 MCP Apps——它只是说「UI 资源我认得」,渲染在前端。
我们作为 client 声明的能力。 **只报真支持的**:`elicitation` 我们能接(`mcp.client` 有回调挂点), `roots` / `sampling` 是 2026-07-28 起 deprecated 的特性,不报。 `extensions` 里报 MCP Apps——它只是说「UI 资源我认得」,渲染在前端。
JSON-RPC 标准码 + MCP 在 -32020..-32099 这段里定义的码。
-32000..-32019 是历史遗留段,规范明令新实现不得再分配;
-32002(资源不存在)只在读老 server 时接受,自己不发。
JSON-RPC 标准码 + MCP 在 `-32020..-32099` 这段里定义的码。 **`-32000..-32019` 是历史遗留段**,规范明令新实现不得再分配; `-32002`(资源不存在)只在读老 server 时接受,自己不发。
(error-response id code message)(error-response id code message data)服务端:错误响应。code 可以是关键字(查 error-codes)或整数。
服务端:错误响应。`code` 可以是关键字(查 `error-codes`)或整数。
(input-required? result)MRTR:server 说「还差点信息」(resultType = input_required)。
MRTR:server 说「还差点信息」(`resultType = input_required`)。
走 initialize 握手的版本,从新到旧——回退时按这个顺序挑。
走 `initialize` 握手的版本,**从新到旧**——回退时按这个顺序挑。
(method k)取方法名;给了未知关键字当场抛(装配期错误好过运行期 -32601)。
取方法名;给了未知关键字**当场抛**(装配期错误好过运行期 -32601)。
全部方法名。字符串常量集中一处——散在各处拼错了不会报错,只会收到
-32601 MethodNotFound,而那看起来像「server 不支持」。
全部方法名。**字符串常量集中一处**——散在各处拼错了不会报错,只会收到 `-32601 MethodNotFound`,而那看起来像「server 不支持」。
(missing-capability-error id required)-32021:告诉客户端缺哪几个能力,否则它不知道该补什么。
`-32021`:告诉客户端**缺哪几个能力**,否则它不知道该补什么。
走每请求 _meta 的版本(无 initialize 握手)。
走**每请求 `_meta`** 的版本(无 `initialize` 握手)。
(notification method-name)(notification method-name params)(notification method-name params {:keys [protocol-version] :as opts})组一条通知(没有 id,对方 MUST NOT 回)。
组一条通知(**没有 id**,对方 MUST NOT 回)。
(parse s)JSON 文本 → 消息 map(键保持字符串,与信封构造侧一致)。解析失败返回 nil。
JSON 文本 → 消息 map(键保持**字符串**,与信封构造侧一致)。解析失败返回 nil。
(protocol-version-of msg)从已组好的请求信封里反读版本——HTTP 传输要拿它写 MCP-Protocol-Version
头。反读而不是各记一份,是因为头与信封不一致 server 会回 -32020。
从**已组好的请求信封**里反读版本——HTTP 传输要拿它写 `MCP-Protocol-Version` 头。反读而不是各记一份,是因为头与信封不一致 server 会回 `-32020`。
(request method-name params {:keys [id protocol-version] :as opts})组一条 JSON-RPC 请求。
opts:
:id 请求 id(必填;协议禁止 null,也禁止重复未回的 id):protocol-version 给了就写 _meta(modern 信封),传 nil 就不写(legacy):client-info / :client-capabilities / :log-level / :extra-meta「写不写 _meta」由调用方决定,不在这里按版本号猜:时代是 client 判出来的
(见 mcp.client),这里只负责照做。曾经在这儿加过一道 (modern? version) 的
守卫,结果是探测一个我们没列进 modern-versions 的新版本时,_meta 被静默
丢掉——请求于是变成 legacy 形状,server 按老语义服务,双方都不报错。
legacy 不写 _meta:老 server 见到陌生的 _meta 未必报错,但可能把它
当成参数的一部分(initialize 那条路最容易出这种事),没必要冒险。
组一条 JSON-RPC 请求。 `opts`: - `:id` 请求 id(必填;协议禁止 null,也禁止重复未回的 id) - `:protocol-version` **给了就写 `_meta`**(modern 信封),传 nil 就不写(legacy) - `:client-info` / `:client-capabilities` / `:log-level` / `:extra-meta` **「写不写 `_meta`」由调用方决定,不在这里按版本号猜**:时代是 client 判出来的 (见 `mcp.client`),这里只负责照做。曾经在这儿加过一道 `(modern? version)` 的 守卫,结果是**探测一个我们没列进 `modern-versions` 的新版本时,`_meta` 被静默 丢掉**——请求于是变成 legacy 形状,server 按老语义服务,双方都不报错。 **legacy 不写 `_meta`**:老 server 见到陌生的 `_meta` 未必报错,但可能把它 当成参数的一部分(`initialize` 那条路最容易出这种事),没必要冒险。
(response-error resp)响应里的错误(没有就 nil)。返回 {:code :message :data}。
响应里的错误(没有就 nil)。返回 `{:code :message :data}`。
(response-result resp)响应里的 result(没有就 nil)。
响应里的 result(没有就 nil)。
(result-response id result)(result-response id result {:keys [protocol-version server-info result-type]})服务端:成功响应。
resultType 是 2026-07-28 新增的必填字段;对 legacy 客户端不发——
它们不认识,而规范说客户端把「缺省」当作 complete。
服务端:成功响应。 **`resultType` 是 2026-07-28 新增的必填字段**;对 legacy 客户端不发—— 它们不认识,而规范说客户端把「缺省」当作 `complete`。
(result-type result)结果类型。缺省当 complete——老 server 不发这个字段,规范明确要求这样兜底。
结果类型。**缺省当 `complete`**——老 server 不发这个字段,规范明确要求这样兜底。
(routing-name msg)Streamable HTTP 的 Mcp-Name 头取哪个值。
规范只对三个方法要求:tools/call / prompts/get 取 params.name,
resources/read 取 params.uri。别的方法不发这个头。
Streamable HTTP 的 `Mcp-Name` 头取哪个值。 规范只对三个方法要求:`tools/call` / `prompts/get` 取 `params.name`, `resources/read` 取 `params.uri`。别的方法不发这个头。
modern 时代的三个「你确实在跟新 server 说话」的信号。
双时代回退的判据全在这三个码上:收到它们说明对面懂 modern,只是这条请求
有问题(版本不支持 / 能力不够 / 信封头不匹配)——不该回退到 initialize,
回退也治不好。收到别的任何 JSON-RPC 错误,才说明对面是 legacy server。
(规范 PR #2844 明确要求回退不得只认单个错误码。)
**modern 时代的三个「你确实在跟新 server 说话」的信号**。 双时代回退的判据全在这三个码上:收到它们说明对面**懂** modern,只是这条请求 有问题(版本不支持 / 能力不够 / 信封头不匹配)——**不该回退到 `initialize`**, 回退也治不好。收到**别的**任何 JSON-RPC 错误,才说明对面是 legacy server。 (规范 PR #2844 明确要求回退不得只认单个错误码。)
MCP Apps 扩展在 capabilities.extensions 里的标识。
MCP Apps 扩展在 `capabilities.extensions` 里的标识。
MCP Apps 扩展:工具 _meta 里那个 ui 对象的键。
规范(/extensions/apps/overview)写的是 _meta.ui.resourceUri——
一个嵌套对象,不是扁平的 ui/resourceUri 那种写法。同一个对象里还可能有 csp
(app 能加载哪些外部源)与 permissions(麦克风/摄像头之类)。
猜错这个键不会报错,只会「明明是 App 工具却没画出界面」——所以这里写死,
并在 mcp.tools/ui-resource-uri 里同时兼容那个扁平写法。
MCP Apps 扩展:工具 `_meta` 里那个 **`ui` 对象**的键。 规范(`/extensions/apps/overview`)写的是 **`_meta.ui.resourceUri`**—— 一个嵌套对象,不是扁平的 `ui/resourceUri` 那种写法。同一个对象里还可能有 `csp` (app 能加载哪些外部源)与 `permissions`(麦克风/摄像头之类)。 猜错这个键**不会报错**,只会「明明是 App 工具却没画出界面」——所以这里写死, 并在 `mcp.tools/ui-resource-uri` 里同时兼容那个扁平写法。
(unsupported-version-error id requested supported)-32022:带上我们支持的版本列表,客户端据此重挑一个重试。
不带 supported 的话客户端只能猜——规范因此把它写成 data 的必备内容。
`-32022`:带上**我们支持的版本列表**,客户端据此重挑一个重试。 不带 `supported` 的话客户端只能猜——规范因此把它写成 data 的必备内容。
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |