MCP项目笔记七(插件 calculator)
C++ 构建一个插件式计算器工具系统
1. 整体功能概述
代码实现的是一个 C++ 插件式数学计算器工具系统,是一个可以被宿主程序(如 AI Agent、命令行工具等)动态加载的插件。核心能力有三:
- 字符串数学表达式解析:支持
3 + sin(1)、(2^3)^2、sqrt(9) * log(10)这类复合表达式的实时求值。 - 独立数学工具:提供加、减、乘、除、幂、开方、阶乘等 8 个基础工具,参数通过 JSON 传入。
- 标准插件接口:通过
PluginAPI导出,宿主程序可以动态发现工具列表、发送请求、获取结果,整个通信协议基于 JSON。
用一句话概括:
「基于递归解析的表达式计算引擎 + JSON 驱动的插件工具系统」
2. 插件的五层架构
整个插件可以清晰地划分为以下五层,每层职责独立、边界清楚:
| 层级 | 名称 | 主要内容 |
|---|---|---|
| L0 | 插件接口导出层 | CreatePlugin / DestroyPlugin,供宿主动态加载,遵循 C ABI |
| L1 | 插件生命周期管理层 | GetName / GetVersion / Initialize / Shutdown / GetToolCount / GetTool |
| L2 | 请求分发层 | HandleRequestImpl:解析 JSON 请求 → 按 toolName 路由 → 返回统一 JSON 响应 |
| L3 | 工具注册表(Metadata) | PluginTool methods[]:工具名、描述、JSON Schema 参数定义 |
| L4 | 计算核心层 | ExpressionParser(递归下降)+ factorial() 等独立数学函数 |
最底层是真正的「数学能力」,最顶层是给宿主程序看的「插件入口」。中间的 HandleRequestImpl 起到总调度作用,是整个插件最核心的函数。
3. 核心:递归下降表达式解析器
ExpressionParser 是这套插件最有技术含量的部分,它用递归下降法(Recursive Descent Parsing)实现了一个完整的数学表达式解析器,无需第三方库。
运算优先级分层
解析器将语法按运算优先级分为四层,形成调用链:
parseExpression // 最低优先级:+ 和 -
└── parseTerm // 次低:* / %
└── parsePower // 幂运算:^ (右结合)
└── parseFactor // 最高:数字、括号、函数、正负号
├── parseFunction // 识别 sin(x) 等
└── parseNumber // 读取 123、45.6 等
右结合的幂运算
幂运算 ^ 在数学上是右结合的,即 (2{32} = 2{(32)} = 512),而不是 ((23)2 = 64)。代码通过在 parsePower 中递归调用自身来实现这一点:
static double parsePower(const std::string& expr, size_t& pos) {
double base = parseFactor(expr, pos);
if (pos < expr.length() && expr[pos] == '^') {
pos++;
double exponent = parsePower(expr, pos); // 递归 → 右结合
return std::pow(base, exponent);
}
return base;
}
支持的内置函数
parseFunction 识别函数名后,映射到标准库函数:
| 函数名 | 对应 std 函数 | 说明 |
|---|---|---|
sin / cos / tan |
std::sin / cos / tan |
三角函数(弧度制) |
sqrt |
std::sqrt |
平方根 |
abs |
std::abs |
绝对值 |
log |
std::log |
自然对数 (\ln) |
exp |
std::exp |
(e^x) |
floor / ceil / round |
同名 | 取整函数 |
4. 工具注册表——Metadata 定义
插件使用静态数组 methods[] 定义工具清单,每个条目包含三个字段:工具名、描述、JSON Schema。这是整个「接口契约」的来源,宿主程序通过 GetToolImpl 枚举所有工具,再按 Schema 构造请求。
static PluginTool methods[] = {
{
"calculator",
"Evaluates a mathematical expression",
"{\"type\":\"object\","
"\"properties\":{\"expression\":{\"type\":\"string\"}},"
"\"required\":[\"expression\"]}"
},
{
"add",
"Adds two numbers",
"{\"type\":\"object\","
"\"properties\":{\"a\":{\"type\":\"number\"},\"b\":{\"type\":\"number\"}},"
"\"required\":[\"a\",\"b\"]}"
},
// ... 其余工具
};
这种「先声明接口再实现功能」的设计,让整个系统具备良好的自描述性,非常适合被 AI Agent 或自动化框架动态发现和调用。
5. HandleRequest 的完整流程
HandleRequestImpl 是插件的总调度中心,负责完整的「请求 → 处理 → 响应」闭环。
执行步骤:
- 初始化响应结构:
content: [],isError: false - 解析请求:
json::parse(req)解析外部传入的 JSON 字符串 - 读取路由信息:取出
params.name(工具名)和params.arguments(参数对象) - 按 toolName 分发:进入对应 if / else if 分支,调用计算逻辑
- ✅ 成功:构造
{"type":"text","text":"3+5 = 8"},推入 content 数组 - ❌ 异常:
catch捕获,置isError: true,填入错误信息 - 序列化返回:
response.dump()→ 分配char*→ 返回给宿主
成功与失败的统一返回格式
不管成功还是失败,返回格式始终一致,宿主程序只需检查 isError 字段即可:
// 成功
{
"content": [{ "type": "text", "text": "3 + 5 = 8" }],
"isError": false
}
// 失败
{
"content": [{ "type": "text", "text": "Error: Division by zero" }],
"isError": true
}
为什么返回 char* 而不是 std::string?
因为插件接口采用 C ABI(应用二进制接口),使用 extern "C" 导出,确保兼容 C 语言或其他非 C++ 宿主程序。std::string 是 C++ 特有类型,跨模块传递可能引发二进制不兼容问题,因此选择 char*。
6. 总结
这份代码展示了一个设计合理、职责清晰的 C++ 插件工程实践。它将数学计算能力封装成 JSON 驱动的工具接口,让宿主程序(如 AI Agent)可以像调用 API 一样使用它。
「先声明接口,再接收请求,处理参数并执行功能,最后把结果按 JSON 协议返回。」
一个 tool 插件的最小闭环,不外乎这四步:
定义 Metadata → 解析参数 → 执行逻辑 → 返回 JSON
掌握这个骨架,就可以把任何计算能力或业务逻辑封装为可被动态调用的工具插件。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)