MCP 客户端:双时代。
(def c (client {:url "https://example.com/mcp"})) ;; 或 {:command ["npx" …]}
(connect! c) ;; 幂等;自动判时代
(list-tools c)
(call-tool c "get_weather" {:city "北京"})
(close! c)
先按 modern 发一条 server/discover,然后看回来的是什么:
| 回来的 | 判定 | 为什么 |
|---|---|---|
DiscoverResult,supportedVersions 含我们这版 | modern,结束 | 对面是新 server |
DiscoverResult,但不含我们这版 | 挑一个双方都支持的:还有 modern 版就换版重试,只剩 legacy 就退回 initialize | 它懂 discover,只是版本对不上 |
-32022 UnsupportedProtocolVersion | 同上(按 data.supported 重挑) | 这是新 server 的信号,不是老 server |
-32021 / -32020 | 原样抛,不回退 | 能力不够 / 信封头不匹配——退回 initialize 也治不好,只会把真正的错误藏起来 |
| 其他任何 JSON-RPC 错误 | 回退 initialize | -32601(没这方法)、-32602(被 _meta 搞晕)、-32700…… 都说明对面是老 server。判据不能只认一个码,这是规范明写的 |
| HTTP 400 / 404 且响应体里没有可识别的 modern 错误 | 回退 | 老 server 在 HTTP 层就把无会话的 POST 拒了(404 = 它要 Mcp-Session-Id) |
| 超时没回 | 回退 | stdio 上老 server 见到陌生方法可能干脆不吭声 |
时代是 server 的属性,不是单条请求的:判一次,记在 client 上,后续请求 都按那个时代走。
server → client 的请求(elicitation / MRTR 的补充输入)只在 stdio 上到得了:
stdio 的读线程会把它交给 :on-notification;而 HTTP 那侧,这类请求是从
POST 响应的 SSE 流里回来的,本模块的 HTTP 传输只取流里第一条 data:
(见 mcp.transport),所以 HTTP 上收不到。
input_required 的结果原样返回(protocol/input-required? 可判),
调用方要接就自己接——不替想象中的用户把 MRTR 的整套状态机写出来。
MCP 客户端:**双时代**。
```clojure
(def c (client {:url "https://example.com/mcp"})) ;; 或 {:command ["npx" …]}
(connect! c) ;; 幂等;自动判时代
(list-tools c)
(call-tool c "get_weather" {:city "北京"})
(close! c)
```
## 判时代的算法(照规范 PR #2844 与官方 SDK 的实现写)
先按 **modern** 发一条 `server/discover`,然后看回来的是什么:
| 回来的 | 判定 | 为什么 |
|---|---|---|
| `DiscoverResult`,`supportedVersions` 含我们这版 | **modern**,结束 | 对面是新 server |
| `DiscoverResult`,但不含我们这版 | 挑一个双方都支持的:还有 modern 版就换版重试,只剩 legacy 就退回 `initialize` | 它懂 discover,只是版本对不上 |
| `-32022` UnsupportedProtocolVersion | 同上(按 `data.supported` 重挑) | 这是**新 server 的信号**,不是老 server |
| `-32021` / `-32020` | **原样抛,不回退** | 能力不够 / 信封头不匹配——退回 `initialize` 也治不好,只会把真正的错误藏起来 |
| 其他任何 JSON-RPC 错误 | 回退 `initialize` | `-32601`(没这方法)、`-32602`(被 `_meta` 搞晕)、`-32700`…… 都说明对面是老 server。**判据不能只认一个码**,这是规范明写的 |
| HTTP 400 / 404 且响应体里没有可识别的 modern 错误 | 回退 | 老 server 在 HTTP 层就把无会话的 POST 拒了(404 = 它要 `Mcp-Session-Id`) |
| 超时没回 | 回退 | stdio 上老 server 见到陌生方法可能干脆不吭声 |
**时代是 server 的属性,不是单条请求的**:判一次,记在 client 上,后续请求
都按那个时代走。
## 一处**没做**、且要说清楚的
**server → client 的请求(elicitation / MRTR 的补充输入)只在 stdio 上到得了**:
stdio 的读线程会把它交给 `:on-notification`;而 HTTP 那侧,这类请求是从
POST 响应的 SSE 流里回来的,本模块的 HTTP 传输只取流里第一条 `data:`
(见 `mcp.transport`),所以 HTTP 上收不到。
`input_required` 的结果**原样返回**(`protocol/input-required?` 可判),
调用方要接就自己接——不替想象中的用户把 MRTR 的整套状态机写出来。(call-tool c tool-name args)tools/call → 结果原样:{content, structuredContent?, isError?}。
不替调用方解释 isError:那是「工具自己失败了」,与「协议出错」是两回事
(后者会抛)。要文本用 mcp.tools/result-text。
`tools/call` → 结果原样:`{content, structuredContent?, isError?}`。
**不替调用方解释 `isError`**:那是「工具自己失败了」,与「协议出错」是两回事
(后者会抛)。要文本用 `mcp.tools/result-text`。(client {:keys [transport url command] :as opts})构造客户端(不连接——连接在 connect!,因为判时代要发请求)。
传输三选一:
:transport 直接给一个 Transport(或任意 (fn [msg] resp)):url Streamable HTTP:command stdio 子进程其余:
:client-info 自报家门,缺省 clj-agent-mcp:capabilities 声明的客户端能力,缺省 protocol/default-client-capabilities:protocol-version 钉死版本(给了就不做时代探测,直接按它走):server-id 本地标识,日志与工具归属用:headers / :timeout-ms / :env / :dir / :on-notification 透传给传输构造客户端(**不连接**——连接在 `connect!`,因为判时代要发请求)。 传输三选一: - `:transport` 直接给一个 `Transport`(或**任意 `(fn [msg] resp)`**) - `:url` Streamable HTTP - `:command` stdio 子进程 其余: - `:client-info` 自报家门,缺省 `clj-agent-mcp` - `:capabilities` 声明的客户端能力,缺省 `protocol/default-client-capabilities` - `:protocol-version` **钉死**版本(给了就不做时代探测,直接按它走) - `:server-id` 本地标识,日志与工具归属用 - `:headers` / `:timeout-ms` / `:env` / `:dir` / `:on-notification` 透传给传输
(connect! c)连接并判时代。幂等——连过了直接返回状态。
返回 {:era :modern|:legacy :protocol-version :capabilities :server-info :instructions}。
连接并判时代。**幂等**——连过了直接返回状态。
返回 `{:era :modern|:legacy :protocol-version :capabilities :server-info :instructions}`。(error-code t)从异常里取 MCP 错误码(不是 MCP 错误就 nil)。
从异常里取 MCP 错误码(不是 MCP 错误就 nil)。
(get-prompt c prompt-name)(get-prompt c prompt-name args)prompts/get → {description?, messages [...]}。
`prompts/get` → `{description?, messages [...]}`。
(instructions c)server 自述的用法说明——适合塞进 system prompt,规范就是这么建议的。
server 自述的用法说明——**适合塞进 system prompt**,规范就是这么建议的。
(list-tools c)tools/list → 工具数组(原样,含 _meta / outputSchema / annotations)。
`tools/list` → 工具数组(**原样**,含 `_meta` / `outputSchema` / `annotations`)。
(mcp-error {:keys [code message data]} method)server 回了 JSON-RPC 错误。ex-data 带 :code / :message / :data。
server 回了 JSON-RPC 错误。`ex-data` 带 `:code` / `:message` / `:data`。
(read-resource c uri)resources/read → {contents [...]}。
`resources/read` → `{contents [...]}`。
(request! c method-key)(request! c method-key params)发一条请求(自动补时代与信封)。method-key 是 protocol/method-names 的键。
发一条请求(自动补时代与信封)。`method-key` 是 `protocol/method-names` 的键。
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 |