跳转到主要内容

概览

Mirobody 中的工具是 res/mcp_tools/ 中的 C++ 文件。每个文件声明一个 Tool 并在编译时自注册;CMakeLists.txt 会把该目录下每个 .cpp 都 glob 进构建,因此整个流程就是:丢入一个新文件、重新构建,工具即上线 —— 无需手动注册,无需连接 router,无需手写 JSON schema。
这是一个编译期机制,而非运行时插件。C++ 引擎(Mirobody)中没有 Python 工具加载器 —— 参数在一张小表里声明,注册表会把它们展开成 MCP 的 inputSchema 以及 OpenAI / Gemini 的 function 描述符。

一个工具的构成

一个工具声明五样东西(第六个 raw_input_schema 是可选的应急出口):
字段类型用途
namestd::string模型调用时用的工具名。
descriptionstd::string工具做什么、何时使用 —— 模型会逐字阅读这段。
authbooltrue 表示需要已认证的调用者;引擎会注入 UserInfo
paramsstd::vector<Param>声明的参数(nameTypeRequired/Optional、描述)。
handlerfunctionResult(const Args&, const UserInfo&, const ToolContext&)
参数类型有 Type::StringIntegerNumberBooleanArrayObject。用带类型的访问器从 Args 读取参数(args.str("x")args.integer("n", 20)args.raw() 用于任意结构)。返回 Result::ok(json)Result::error("message")

快速开始

参考工具是 echo.cpp —— 最小的完整示例。复制它然后开动。
1

创建文件

res/mcp_tools/ 中新增一个 .cpp
touch res/mcp_tools/weather.cpp
2

声明一个 Tool 和一个 handler

引入 mcp/tool.hpp,编写 handler,声明 Tool,并注册它:
res/mcp_tools/weather.cpp
#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);
文件作用域处的 MIROBODY_REGISTER_TOOL(...) 这一行才是把工具接入的关键 —— 它初始化一个文件作用域的静态量,调用 registry().add(...)
3

重新构建

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

触及用户数据的工具

设置 auth = true,引擎会在分发前解析调用者(JWT 或个人 MCP secret),并把身份作为 UserInfo 传入。对未认证的调用及时退出:
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_filesread_file
ctx.storage存放上传字节 / 签名 URL 的对象存储read_file
ctx.db关系型存储family_healthsummarize_conversation
ctx.memory长期记忆存储recall_memoryremember
像内置工具那样,用一个干净的错误来容忍缺失的后端:
if (ctx.memory == nullptr)
    return Result::error("Long-term memory is not enabled on this server");
关于读取模式 —— 强制按用户所有权、返回 FHIR observation、持久化到关系型存储 —— 请配合本页一起阅读内置工具:list_files.cppread_file.cppfamily_health.cppremember.cpp

非扁平参数

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

最佳实践

description 和每个 Param 的描述,是模型在决定是否以及如何调用你的工具时唯一拥有的上下文。要说清楚它做什么、何时使用,并在参数描述里给出一个具体的示例值。要让工具被正确调用,这一点比其它任何事都重要。
用 rapidjson 构建一个小小的 JSON 对象并返回 Result::ok(to_json(d))。保持结果紧凑 —— 不要内联模型用不上的大块数据(看看 read_file 如何为二进制返回一个签名 url 而非字节本身)。
对错误输入、缺失 auth 或未配置的后端,返回 Result::error("...")。注册表还会捕获任何 handler 异常并转成错误,因此一个工具绝不会让服务器崩溃。
auth 工具,一切都从 user.user_id 派生。绝不要用 subject_user_id 来写入或暴露其它数据 —— 它是一个只读的护理圈提示,只有健康读取类工具才应遵从。

验证工具已加载

tools/list 请求 /mcp 端点,确认你的工具在返回的数组中:
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(...) 结尾。

下一步

内置工具

默认提供的 9 个工具参考

MCP 集成

将你的工具连接到 Claude、Cursor 和 ChatGPT

工具与 Agent 概览

Agent、工具与 provider 如何协同