# 模型上下文协议(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`):提供商在运行时被发现, 聊天循环使用当前提供商的工具方言发送此处注册的工具。