Liking cljdoc? Tell your friends :D

im.ttalk.agent.mcp.server

MCP 服务端:把一组内联工具(以及资源、提示词)暴露成 MCP server。

(def s (server {:name "clj-agent" :version "0.3"
                :instructions "这台机器上的天气与文件工具。"
                :tools [{:name "get_weather"
                         :description "查天气"
                         :parameters {:type "object"
                                      :properties {:city {:type "string"}}
                                      :required ["city"]}
                         :handler (fn [args _ctx] (str "晴," (get args "city")))}]}))

(stdio-server! s)        ;; 阻塞:把 stdin/stdout 当传输
(handle-message s msg)   ;; 或者自己接到别的传输上——这是个**纯函数**

分层:本 ns 不碰 web

核心是 handle-message:一条进、一条出(通知返回 nil),没有 I/O、没有会话。 把它接到 Ring / http-kit / 别的什么上,是调用方几行的事,也是 design-principles §2 划的那条线(HTTP 服务端依赖不进库)。stdio 循环留在模块里——它不碰 web。

双时代

同一个 handle-message 同时服务两种客户端,靠请求自己的形状分流:

请求判定依据
params._meta 里有 protocolVersionmodern,无状态服务规范的兼容矩阵:带 modern _meta 的请求按本版语义处理
method = initializelegacy,回一份 InitializeResult同上:initialize 选择 legacy 语义
两者都不是当 legacy 请求处理我们是无状态的,本来也不靠会话记东西

modern 侧的三条校验(错一条就静默少一半功能,所以都做实):

  1. _meta.protocolVersion 与 _meta.clientCapabilities 必填,缺 → -32602;
  2. 版本不认识 → -32022,且 data.supported 必须列出我们支持的版本, 客户端要靠它重挑;
  3. HTTP 的 MCP-Protocol-Version / Mcp-Method / Mcp-Name 头要与信封一致, 不一致 → -32020。头只有传输层看得见,所以这条由 check-headers 提供,web 层在调 handle-message 之前先过一遍。
MCP 服务端:把一组**内联工具**(以及资源、提示词)暴露成 MCP server。

```clojure
(def s (server {:name "clj-agent" :version "0.3"
                :instructions "这台机器上的天气与文件工具。"
                :tools [{:name "get_weather"
                         :description "查天气"
                         :parameters {:type "object"
                                      :properties {:city {:type "string"}}
                                      :required ["city"]}
                         :handler (fn [args _ctx] (str "晴," (get args "city")))}]}))

(stdio-server! s)        ;; 阻塞:把 stdin/stdout 当传输
(handle-message s msg)   ;; 或者自己接到别的传输上——这是个**纯函数**
```

## 分层:本 ns **不碰 web**

核心是 `handle-message`:一条进、一条出(通知返回 nil),没有 I/O、没有会话。
把它接到 Ring / http-kit / 别的什么上,是调用方几行的事,也是 `design-principles`
§2 划的那条线(HTTP 服务端依赖不进库)。stdio 循环留在模块里——它不碰 web。

## 双时代

同一个 `handle-message` 同时服务两种客户端,靠**请求自己的形状**分流:

| 请求 | 判定 | 依据 |
|---|---|---|
| `params._meta` 里有 `protocolVersion` | **modern**,无状态服务 | 规范的兼容矩阵:带 modern `_meta` 的请求按本版语义处理 |
| `method = initialize` | **legacy**,回一份 `InitializeResult` | 同上:`initialize` 选择 legacy 语义 |
| 两者都不是 | 当 legacy 请求处理 | 我们是无状态的,本来也不靠会话记东西 |

modern 侧的三条校验(错一条就静默少一半功能,所以都做实):

1. `_meta.protocolVersion` 与 `_meta.clientCapabilities` **必填**,缺 → `-32602`;
2. 版本不认识 → `-32022`,且 `data.supported` 必须**列出我们支持的版本**,
   客户端要靠它重挑;
3. HTTP 的 `MCP-Protocol-Version` / `Mcp-Method` / `Mcp-Name` 头要与信封一致,
   不一致 → `-32020`。头只有传输层看得见,所以这条由 `check-headers`
   提供,web 层在调 `handle-message` 之前先过一遍。
raw docstring

capabilitiesclj

(capabilities s)

只报真有东西的那几位。空的工具表还报 tools 是谎报能力位——客户端会照着 去 tools/list,拿回一个空数组,然后不知道该怪谁。

只报**真有东西**的那几位。空的工具表还报 `tools` 是谎报能力位——客户端会照着
去 `tools/list`,拿回一个空数组,然后不知道该怪谁。
sourceraw docstring

check-headersclj

(check-headers msg headers)

HTTP 层用:头与信封是否一致。一致返回 nil,不一致返回一条 -32020 错误响应。

只有传输层看得见头,所以这条校验不能藏在 handle-message 里;但规则属于 协议,所以也不能让每个 web 层各写一遍。折中就是这个函数:web 层拿到请求后 先过一遍它,再把 body 交给 handle-message。

headers 是小写键的 map(HTTP 头大小写不敏感,Ring 也给小写)。

HTTP 层用:头与信封是否一致。一致返回 nil,不一致返回一条 `-32020` 错误响应。

**只有传输层看得见头**,所以这条校验不能藏在 `handle-message` 里;但规则属于
协议,所以也不能让每个 web 层各写一遍。折中就是这个函数:web 层拿到请求后
先过一遍它,再把 body 交给 `handle-message`。

`headers` 是**小写键**的 map(HTTP 头大小写不敏感,Ring 也给小写)。
sourceraw docstring

handle-messageclj

(handle-message s msg)

一条进、一条出的纯函数。通知返回 nil(协议禁止给通知回响应)。

modern 请求走无状态语义并带上 resultType / _meta.serverInfo; legacy 请求(initialize 或没有 _meta 的)按老语义回。

**一条进、一条出**的纯函数。通知返回 nil(协议禁止给通知回响应)。

modern 请求走无状态语义并带上 `resultType` / `_meta.serverInfo`;
legacy 请求(`initialize` 或没有 `_meta` 的)按老语义回。
sourceraw docstring

serverclj

(server {:keys [name version tools resources prompts instructions ctx
                expose-error-messages?]
         :or {name "clj-agent-mcp" version "0.3"}})

构造一个 server(纯值,没有连接、没有线程)。

  • :name / :version 自报家门(Implementation)
  • :instructions 用法说明,客户端可能塞进 system prompt
  • :tools 内联工具数组 {:name :description :parameters :handler}, :handler 是 (fn [args ctx] result)——与 clj-agent 的 内联工具同形,所以现成的工具直接放进来就能暴露出去
  • :resources [{:uri :name :description :mimeType :read (fn [uri] contents)}]
  • :prompts [{:name :description :arguments :get (fn [args] messages)}]
  • :ctx 传给工具 handler 的第二个参数(缺省 {})
  • :expose-error-messages? 工具抛异常时,把异常消息也回给客户端(缺省 false)

为什么异常消息默认不外泄

server 的对面是外部客户端,而异常消息里常有路径、内部结构、上游 URL 与 request-id。缺省只回异常类名(够模型判断「该不该换个说法重试」),完整消息 记进 server 日志。本地开发要看细节,把 :expose-error-messages? 打开—— 这是一次显式的取舍,不是偷偷做的决定。

构造一个 server(**纯值**,没有连接、没有线程)。

- `:name` / `:version`  自报家门(`Implementation`)
- `:instructions`       用法说明,客户端可能塞进 system prompt
- `:tools`              内联工具数组 `{:name :description :parameters :handler}`,
                        `:handler` 是 `(fn [args ctx] result)`——**与 clj-agent 的
                        内联工具同形**,所以现成的工具直接放进来就能暴露出去
- `:resources`          `[{:uri :name :description :mimeType :read (fn [uri] contents)}]`
- `:prompts`            `[{:name :description :arguments :get (fn [args] messages)}]`
- `:ctx`                传给工具 handler 的第二个参数(缺省 `{}`)
- `:expose-error-messages?` 工具抛异常时,把**异常消息**也回给客户端(缺省 `false`)

## 为什么异常消息默认不外泄

server 的对面是**外部客户端**,而异常消息里常有路径、内部结构、上游 URL 与
request-id。缺省只回**异常类名**(够模型判断「该不该换个说法重试」),完整消息
记进 server 日志。本地开发要看细节,把 `:expose-error-messages?` 打开——
这是一次**显式**的取舍,不是偷偷做的决定。
sourceraw docstring

stdio-server!clj

(stdio-server! s)
(stdio-server! s in out)

把 stdin/stdout 当传输跑起来(阻塞,直到 stdin 关闭)。

两条必须守住的:

  1. 响应只写 stdout,日志只写 stderr。往 stdout 打一行日志,对面的解析器就 废了——这是 stdio server 最常见的翻车方式;
  2. 一行一条,不能有裸换行。p/encode 出来的 JSON 本身不含换行。
把 stdin/stdout 当传输跑起来(**阻塞**,直到 stdin 关闭)。

两条必须守住的:

1. **响应只写 stdout,日志只写 stderr**。往 stdout 打一行日志,对面的解析器就
   废了——这是 stdio server 最常见的翻车方式;
2. **一行一条,不能有裸换行**。`p/encode` 出来的 JSON 本身不含换行。
sourceraw docstring

supported-versionsclj

我们同时支持的版本,从新到旧。modern 一个、legacy 两个。 server/discover 与 -32022 的 data.supported 都报这一份,不能各报各的。

我们**同时**支持的版本,从新到旧。modern 一个、legacy 两个。
`server/discover` 与 `-32022` 的 `data.supported` 都报这一份,不能各报各的。
sourceraw docstring

cljdoc builds & hosts documentation for Clojure/Script libraries

Keyboard shortcuts
Ctrl+kJump to recent docs
←Move to previous article
→Move to next article
Ctrl+/Jump to the search field
× close