客户端传输:Streamable HTTP 与 stdio。
一个协议、两个实现,外加「传输就是一个函数」这条后门:
(send! t msg) ;; 请求 → 响应 map;通知 → nil
(close! t) ;; 释放(stdio 杀子进程;HTTP 无状态,无操作)
(fn [msg] resp) 也是合法传输(send! 对 IFn 有实现)——测试因此不用起
服务、不用起进程,一个纯函数就能把整条客户端逻辑跑完。这条不是给测试开的
特例,它就是这层的最小契约。
POST 一条 JSON-RPC,Accept: application/json, text/event-stream,响应可能是
两种之一。三个头是必填且不能与信封不一致:
| 头 | 值 | 不对会怎样 |
|---|---|---|
MCP-Protocol-Version | 与 _meta 里的版本同一个值 | -32020 HeaderMismatch |
Mcp-Method | 与 body 的 method 同一个值 | 同上 |
Mcp-Name | 只对 tools/call / prompts/get(取 name)与 resources/read(取 uri) | 同上 |
版本是从信封里反读的(protocol/protocol-version-of),不是传输自己记一份
——两处各记一份就是 -32020 的来源。
没有会话:modern 不再有 Mcp-Session-Id。但双时代客户端要连老 server,
所以这里仍然会收下并回传 server 给的 session id;modern 路径上 server 不会
给,这段代码就是死的。
一行一条 JSON(消息内不得有裸换行)。读端是一条后台线程:响应按 id 交给
等待中的 promise,其余(通知、日志)交给 :on-notification。
为什么不能「写一行、读一行」了事:协议允许 server 在响应之前先推通知 (进度、日志、订阅事件),同步读会把它们当成响应。而且 2026-07-28 明说 「一条 stdio 连接不是一个会话」,客户端可以在同一个进程上交错发无关请求。
客户端传输:Streamable HTTP 与 stdio。 一个协议、两个实现,外加「传输就是一个函数」这条后门: ```clojure (send! t msg) ;; 请求 → 响应 map;通知 → nil (close! t) ;; 释放(stdio 杀子进程;HTTP 无状态,无操作) ``` **`(fn [msg] resp)` 也是合法传输**(`send!` 对 `IFn` 有实现)——测试因此不用起 服务、不用起进程,一个纯函数就能把整条客户端逻辑跑完。这条不是给测试开的 特例,它就是这层的最小契约。 ## Streamable HTTP(2026-07-28) POST 一条 JSON-RPC,`Accept: application/json, text/event-stream`,响应可能是 两种之一。三个头是**必填**且**不能与信封不一致**: | 头 | 值 | 不对会怎样 | |---|---|---| | `MCP-Protocol-Version` | 与 `_meta` 里的版本**同一个值** | `-32020 HeaderMismatch` | | `Mcp-Method` | 与 body 的 `method` 同一个值 | 同上 | | `Mcp-Name` | 只对 `tools/call` / `prompts/get`(取 `name`)与 `resources/read`(取 `uri`) | 同上 | 版本是从**信封里反读**的(`protocol/protocol-version-of`),不是传输自己记一份 ——两处各记一份就是 `-32020` 的来源。 **没有会话**:modern 不再有 `Mcp-Session-Id`。但双时代客户端要连老 server, 所以这里仍然会**收下并回传** server 给的 session id;modern 路径上 server 不会 给,这段代码就是死的。 ## stdio 一行一条 JSON(消息内不得有裸换行)。读端是一条**后台线程**:响应按 id 交给 等待中的 promise,其余(通知、日志)交给 `:on-notification`。 为什么不能「写一行、读一行」了事:协议允许 server 在响应之前先推通知 (进度、日志、订阅事件),同步读会把它们当成响应。而且 2026-07-28 明说 「一条 stdio 连接不是一个会话」,客户端可以在同一个进程上交错发无关请求。
(http-transport {:keys [url headers timeout-ms client proxy]
:or {timeout-ms 30000}})Streamable HTTP 传输(传输层是 im.ttalk.http)。
:url 必填:headers 额外头(鉴权走这里;MCP 的 auth 框架只管 HTTP):timeout-ms 单次请求上限(缺省 30s):client 自带 java.net.http.HttpClient;缺省共享的 im.ttalk.http/default-client
(HTTP/1.1、虚拟线程、connect 30s——designs/http-module.md §6):proxy 没给 :client 时用::env(读 https_proxy / no_proxy …)、代理地址串,或 ProxySelectorStreamable HTTP 传输(传输层是 `im.ttalk.http`)。
- `:url` 必填
- `:headers` 额外头(鉴权走这里;MCP 的 auth 框架只管 HTTP)
- `:timeout-ms` 单次请求上限(缺省 30s)
- `:client` 自带 `java.net.http.HttpClient`;缺省共享的 `im.ttalk.http/default-client`
(HTTP/1.1、虚拟线程、connect 30s——designs/http-module.md §6)
- `:proxy` 没给 `:client` 时用:`:env`(读 https_proxy / no_proxy …)、代理地址串,或 ProxySelector(stdio-transport {:keys [command env dir timeout-ms on-notification]
:or {timeout-ms 30000}})起一个子进程当 MCP server,用它的 stdin/stdout 说话。
:command ["npx" "-y" "@modelcontextprotocol/server-everything"](必填):env 追加的环境变量 map:dir 工作目录:timeout-ms 单次请求上限(缺省 30s):on-notification (fn [msg])——通知与「没人等的响应」都走这里stderr 直接继承父进程:server 的诊断信息本来就该看得见,吞掉它等于把 「为什么连不上」变成猜谜。
起一个子进程当 MCP server,用它的 stdin/stdout 说话。 - `:command` `["npx" "-y" "@modelcontextprotocol/server-everything"]`(必填) - `:env` 追加的环境变量 map - `:dir` 工作目录 - `:timeout-ms` 单次请求上限(缺省 30s) - `:on-notification` `(fn [msg])`——通知与「没人等的响应」都走这里 **stderr 直接继承父进程**:server 的诊断信息本来就该看得见,吞掉它等于把 「为什么连不上」变成猜谜。
(close! t)释放资源。
释放资源。
(send! t msg)发一条消息。请求返回响应 map;通知返回 nil。
发一条消息。请求返回响应 map;通知返回 nil。
(transport-error msg data)传输层失败。带上足够让客户端判时代的证据:HTTP 状态码与响应体里可能有的
JSON-RPC 错误——双时代回退正是看这两样(见 mcp.client)。
传输层失败。**带上足够让客户端判时代的证据**:HTTP 状态码与响应体里可能有的 JSON-RPC 错误——双时代回退正是看这两样(见 `mcp.client`)。
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 |