# 模型上下文协议(MCP)
Deft 可以将任意函数暴露为 LLM 客户端可调用的工具——Claude Desktop、VS Code Copilot、Goose,
以及任何其他支持
Model Context Protocol 的客户端。
这一集成通过 `mcp/*` 命名空间实现:构建一个工具服务器,向其中添加 Deft 函数,然后通过 stdio 或 HTTP 提供服务。
协议是 JSON-RPC 2.0;你无需关心这些细节——`mcp/*` 会处理。
## `mcp/*` 标准库函数
| 函数 | 返回值 | 用途 |
|---|---|---|
| `[mcp/create :name]` | `:ok` | 创建一个空的工具服务器 |
| `[mcp/add :name $fn ?description]` | `:ok` | 将 Deft 函数添加为工具 |
| `[mcp/serve :name]` | 阻塞 | 启动 stdio 服务器 |
| `[mcp/http-serve :name $port]` | 阻塞 | 启动 HTTP 服务器 |
| `[mcp/dispatch :name $json-string]` | json-string | 一次性分派(用于测试) |
## 最小示例
def hello {|name^string| @doc "Greet someone by name." [str "Hello, $name!"] } def add {|a^number b^number| @doc "Add two numbers." ($a + $b) } mcp/create :my-tools mcp/add :my-tools $hello mcp/add :my-tools $add mcp/serve :my-tools # blocks, serving over stdio
保存为 `my-tools.dft`。当 MCP 客户端以 `{:name "Alice"}` 调用 `hello` 时,函数会被调用,
结果以 JSON 形式返回给 LLM。
## 工具如何描述
`mcp/add` 会读取:
- 函数的名称(`def` 名称)。
- 若未显式传入描述,则读取 `@doc` 中的描述。
- 函数签名中的参数列表——参数名和类型标注将成为 LLM 看到的 JSON schema。
因此,一个类型良好、`@doc` 清晰的函数无需任何额外样板代码,就能成为一个描述完善的 MCP 工具:
def search-codebase {|query^string max-results^number| @doc "Search the codebase for a text query. Returns up to max-results matches." # ... implementation ... @{} } mcp/add :my-tools $search-codebase
LLM 看到的工具名为 `search-codebase`,参数为 `query:string` 和 `max-results:number`,
并以你的描述作为提示信息。
## Stdio 传输
`mcp/serve` 通过 stdin/stdout 运行服务器,并永久阻塞。这正是 Claude Desktop 和大多数编辑器集成所期望的方式:
mcp/serve :my-tools
### Claude Desktop 配置
在 `claude_desktop_config.json` 中:
```json
{
"mcpServers": {
"my-tools": {
"command": "/path/to/deft",
"args": ["/abs/path/to/my-tools.dft"]
}
}
}
````
### VS Code 配置
在 `.vscode/mcp.json` 中:
```json
{
"servers": {
"my-tools": {
"type": "stdio",
"command": "/path/to/deft",
"args": ["my-tools.dft"]
}
}
}
````
## HTTP 传输
`mcp/http-serve` 在指定端口上运行 HTTP 监听器。当你需要多个并发客户端或长时间运行的服务时很有用:
mcp/http-serve :my-tools 3001
在 `.vscode/mcp.json` 中:
```json
{
"servers": {
"my-tools": {
"type": "http",
"url": "http://localhost:3001/mcp"
}
}
}
````
### Stdio 与 HTTP —— 何时使用哪种
| 方面 | Stdio | HTTP |
|---|---|---|
| 每进程客户端数 | 一个 | 多个 |
| 生命周期 | 由客户端启动 | 长期运行的服务 |
| 配置复杂度 | 最简单 | 稍复杂 |
| 延迟 | 每次启动全新进程 | 热进程 |
对于本地编辑器集成,stdio 是默认选择——它摩擦最小,且与所有 MCP 客户端支持的方案一致。
当你需要跨多个客户端共享服务或长期运行的后台进程时,再使用 HTTP。
## 工具命名空间
对于共享前缀的工具,用 `::` 命名函数,即可在工具名中获得点分隔的命名空间:
def file-ops::read {|path^string| ... } def file-ops::write {|path^string content^string| ... } def file-ops::list {|dir^string| ... } mcp/add :fs-tools $file-ops::read mcp/add :fs-tools $file-ops::write mcp/add :fs-tools $file-ops::list
LLM 看到的工具名分别为 `file-ops.read`、`file-ops.write`、`file-ops.list`。
## 最佳实践
1. 为每个工具编写文档。 LLM 只知道描述告诉它的内容。没有 `@doc` 的函数不会附带任何用途提示。
2. 为参数添加类型。 类型标注将成为 JSON schema。未标注类型的参数会显示为 `any`,LLM 可能填入无意义的内容。
3. 优先使用小型、可组合的工具。 每个工具应只做一件事。LLM 串联小工具比推理一个庞大的单体工具更可靠。
4. 优雅地处理错误。 返回结构化的 `%Err` 映射而不是抛出错误——抛出的错误会中止调用并让 LLM 困惑。
5. 返回对 JSON 友好的数据。 映射、列表和标量可以干净地序列化。带标签的映射、集合和闭包则不行。
## 一个实际的工具
def project-stats {|project-path^string| @doc { :description "Compute statistics for a Deft project: file count, total lines, dependencies." :params %{ :project-path "Absolute path to the project root" | } :returns "Map with :files :lines :deps keys" } set files [unwrap [fs/glob "$project-path/**/*.dft"]] set total 0 for f in $files { set text [unwrap [fs/read $f]] set lines $text(lines) set total ($total + $lines) | } set deps @{} for f in $files { set text [unwrap [fs/read $f]] for line [split $text "\n"] { if [starts-with? [trim $line] "import "] { set parts [split $line "\""] if ($parts(len) >= 2) { $deps << $parts(nth 1) } | | } | %{ :files $files(len) :lines $total :deps $deps(unique) | } } mcp/create :dev-tools mcp/add :dev-tools $project-stats mcp/serve :dev-tools
## 用于测试的分派
`mcp/dispatch` 是一个一次性入口,适用于测试和脚本。传入 JSON-RPC 请求字符串,返回 JSON-RPC 响应字符串——无需启动服务器:
set req [json/stringify %{ :jsonrpc "2.0" :id 1 :method "tools/call" :params %{ :name "hello" :arguments %{ :name "Alice" } } }] set resp [mcp/dispatch :my-tools $req] echo $resp
无需真实的 LLM 客户端,即可在 `defsuite` 中对工具接口进行单元测试。
## 已内置的内容
TUI 内置的聊天应用本身就暴露了一个功能丰富的 MCP 工具服务器——参见 `packages/stdext/src/mcp_tools.dft`,其中的示例包括:
- `help.lookup` — 按名称获取文档
- `list-stdext` — 枚举 stdext 包(旧别名:`list-stdlib`)
- `grep-codebase` / `search-codebase` — 搜索源文件
- `git-log` / `git-diff` — git 操作
- `find-files` — 按模式定位文件
- `find-references` — 符号交叉引用
- `run-tests` — 调用测试运行器
- `eval-dft` — 在沙箱中求值 Deft 代码片段
- `web-fetch` — 获取 URL
- `write-patch` — 应用代码更改
这些都是有用的 MCP 工具的良好参考。
## 快速参考
| 形式 | 用途 |
|---|---|
| `[mcp/create :name]` | 创建工具服务器 |
| `[mcp/add :name $fn ?desc]` | 注册函数 |
| `[mcp/serve :name]` | Stdio 服务器(阻塞) |
| `[mcp/http-serve :name $port]` | HTTP 服务器(阻塞) |
| `[mcp/dispatch :name $json]` | 一次性分派 |
## 另见
`01-providers`——AI 提供商插件(`deft/stdext/ai`):提供商在运行时被发现,
聊天循环使用当前提供商的工具方言发送此处注册的工具。