> ## Documentation Index
> Fetch the complete documentation index at: https://docs.mirobody.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# 添加自定义工具

> 向 Mirobody 引擎添加一个 C++ MCP 工具 —— 把一个 .cpp 丢进 res/mcp_tools/ 并重新构建。

## 概览

Mirobody 中的工具是 **[`res/mcp_tools/`](https://github.com/thetahealth/mirobody/tree/main/res/mcp_tools) 中的 C++ 文件**。每个文件声明一个 `Tool` 并在编译时自注册；`CMakeLists.txt` 会把该目录下每个 `.cpp` 都 glob 进构建，因此整个流程就是：**丢入一个新文件、重新构建，工具即上线** —— 无需手动注册，无需连接 router，无需手写 JSON schema。

<Info>
  这是一个**编译期**机制，而非运行时插件。C++ 引擎（Mirobody）中没有 Python 工具加载器 —— 参数在一张小表里声明，注册表会把它们展开成 MCP 的 `inputSchema` 以及 OpenAI / Gemini 的 function 描述符。
</Info>

## 一个工具的构成

一个工具声明五样东西（第六个 `raw_input_schema` 是可选的应急出口）：

| 字段            | 类型                   | 用途                                                          |
| ------------- | -------------------- | ----------------------------------------------------------- |
| `name`        | `std::string`        | 模型调用时用的工具名。                                                 |
| `description` | `std::string`        | 工具做什么、何时使用 —— 模型会逐字阅读这段。                                    |
| `auth`        | `bool`               | `true` 表示需要已认证的调用者；引擎会注入 `UserInfo`。                        |
| `params`      | `std::vector<Param>` | 声明的参数（`name`、`Type`、`Required`/`Optional`、描述）。              |
| `handler`     | function             | `Result(const Args&, const UserInfo&, const ToolContext&)`。 |

参数类型有 `Type::String`、`Integer`、`Number`、`Boolean`、`Array`、`Object`。用带类型的访问器从 `Args` 读取参数（`args.str("x")`、`args.integer("n", 20)`、`args.raw()` 用于任意结构）。返回 `Result::ok(json)` 或 `Result::error("message")`。

## 快速开始

参考工具是 [`echo.cpp`](https://github.com/thetahealth/mirobody/blob/main/res/mcp_tools/echo.cpp) —— 最小的完整示例。复制它然后开动。

<Steps>
  <Step title="创建文件">
    在 `res/mcp_tools/` 中新增一个 `.cpp`：

    ```bash theme={null}
    touch res/mcp_tools/weather.cpp
    ```
  </Step>

  <Step title="声明一个 Tool 和一个 handler">
    引入 `mcp/tool.hpp`，编写 handler，声明 `Tool`，并注册它：

    ```cpp res/mcp_tools/weather.cpp theme={null}
    #include "mcp/tool.hpp"
    #include <rapidjson/document.h>

    namespace {
    using namespace mirobody::mcp;

    Result weather(const Args& args, const UserInfo&, const ToolContext&) {
        const std::string city = args.str("city");
        if (city.empty()) return Result::error("city is required");

        rapidjson::Document d;
        d.SetObject();
        rapidjson::Document::AllocatorType& a = d.GetAllocator();
        d.AddMember("city",
                    rapidjson::Value(city.c_str(),
                                     static_cast<rapidjson::SizeType>(city.size()), a), a);
        d.AddMember("temperature_c", 22, a);
        d.AddMember("condition", "Sunny", a);
        return Result::ok(to_json(d));
    }

    const Tool kWeather = {
        "weather",
        "Get the current weather for a city. Call this when the user asks about "
        "the weather or conditions in a named place.",
        false,                                                  // auth
        { Param("city", Type::String, Required, "City name, e.g. \"Boston\"") },
        &weather,
    };
    }   // namespace

    MIROBODY_REGISTER_TOOL(kWeather);
    ```

    <Check>
      文件作用域处的 `MIROBODY_REGISTER_TOOL(...)` 这一行才是把工具接入的关键 —— 它初始化一个文件作用域的静态量，调用 `registry().add(...)`。
    </Check>
  </Step>

  <Step title="重新构建">
    ```bash theme={null}
    ./build.sh
    ```

    `res/mcp_tools/*.cpp` 会被 CMake glob，因此你的新文件无需任何其它改动即可被拾取。重启 `./mirobody`，工具即上线。
  </Step>
</Steps>

## 触及用户数据的工具

设置 `auth = true`，引擎会在分发前解析调用者（JWT 或个人 MCP secret），并把身份作为 `UserInfo` 传入。对未认证的调用及时退出：

```cpp theme={null}
Result my_tool(const Args& args, const UserInfo& user, const ToolContext& ctx) {
    if (!user.authed()) return Result::error("Not authenticated");
    // user.user_id is the caller's row id; scope everything to it.
    // ...
}
```

handler 在其参数与调用者身份之外需要的一切，都通过 `ToolContext` 送达 —— 全部是**借用**指针，其中任何一个在对应后端未配置时都可能为空：

| 句柄            | 触及                    | 使用者                                      |
| ------------- | --------------------- | ---------------------------------------- |
| `ctx.cache`   | 按用户维护的索引（例如上传文件索引）    | `list_files`、`read_file`                 |
| `ctx.storage` | 存放上传字节 / 签名 URL 的对象存储 | `read_file`                              |
| `ctx.db`      | 关系型存储                 | `family_health`、`summarize_conversation` |
| `ctx.memory`  | 长期记忆存储                | `recall_memory`、`remember`               |

像内置工具那样，用一个干净的错误来容忍缺失的后端：

```cpp theme={null}
if (ctx.memory == nullptr)
    return Result::error("Long-term memory is not enabled on this server");
```

<Tip>
  关于读取模式 —— 强制按用户所有权、返回 FHIR observation、持久化到关系型存储 —— 请配合本页一起阅读内置工具：[`list_files.cpp`](https://github.com/thetahealth/mirobody/blob/main/res/mcp_tools/list_files.cpp)、[`read_file.cpp`](https://github.com/thetahealth/mirobody/blob/main/res/mcp_tools/read_file.cpp)、[`family_health.cpp`](https://github.com/thetahealth/mirobody/blob/main/res/mcp_tools/family_health.cpp)、[`remember.cpp`](https://github.com/thetahealth/mirobody/blob/main/res/mcp_tools/remember.cpp)。
</Tip>

## 非扁平参数

声明的 `params` 表会生成一个由扁平标量和数组构成的 JSON Schema，这覆盖了大多数工具。若某个工具的参数不是扁平的，用 `args.raw()` 读取原始对象（正如 [`render_chart`](https://github.com/thetahealth/mirobody/blob/main/res/mcp_tools/render_chart.cpp) 处理它的 ECharts `option` 那样），并且 —— 如果你需要对外声明的 schema 完全一致 —— 把 `Tool` 上可选的 `raw_input_schema` 字段设为一个会被逐字使用的 schema 字符串。

## 最佳实践

<AccordionGroup>
  <Accordion title="为模型撰写描述" icon="file-lines">
    `description` 和每个 `Param` 的描述，是模型在决定是否以及如何调用你的工具时唯一拥有的上下文。要说清楚**它做什么、何时使用**，并在参数描述里给出一个具体的示例值。要让工具被正确调用，这一点比其它任何事都重要。
  </Accordion>

  <Accordion title="返回结构化 JSON" icon="code">
    用 rapidjson 构建一个小小的 JSON 对象并返回 `Result::ok(to_json(d))`。保持结果紧凑 —— 不要内联模型用不上的大块数据（看看 `read_file` 如何为二进制返回一个签名 `url` 而非字节本身）。
  </Accordion>

  <Accordion title="干净地失败" icon="triangle-exclamation">
    对错误输入、缺失 auth 或未配置的后端，返回 `Result::error("...")`。注册表还会捕获任何 handler 异常并转成错误，因此一个工具绝不会让服务器崩溃。
  </Accordion>

  <Accordion title="限定到调用者" icon="lock">
    对 `auth` 工具，一切都从 `user.user_id` 派生。绝不要用 `subject_user_id` 来写入或暴露其它数据 —— 它是一个只读的护理圈提示，只有健康读取类工具才应遵从。
  </Accordion>
</AccordionGroup>

## 验证工具已加载

用 `tools/list` 请求 [`/mcp` 端点](/zh/tools/mcp-integration)，确认你的工具在返回的数组中：

```bash theme={null}
curl -X POST http://localhost:8080/mcp \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
```

如果没有，检查构建日志里的编译错误，并确认文件位于 `res/mcp_tools/` 下、以一行 `MIROBODY_REGISTER_TOOL(...)` 结尾。

## 下一步

<CardGroup cols={2}>
  <Card title="内置工具" icon="wrench" href="/zh/tools/built-in">
    默认提供的 9 个工具参考
  </Card>

  <Card title="MCP 集成" icon="bolt" href="/zh/tools/mcp-integration">
    将你的工具连接到 Claude、Cursor 和 ChatGPT
  </Card>

  <Card title="工具与 Agent 概览" icon="brain" href="/zh/tools/overview">
    Agent、工具与 provider 如何协同
  </Card>
</CardGroup>
