Skip to content

♻️ refactor(mcp): 统一 MCP 工具注册与协议入口 - #155

Merged
HuXiaohui424 merged 14 commits into
1024XEngineer:mainfrom
HuXiaohui424:feature/mcp-skeleton
Aug 7, 2026
Merged

♻️ refactor(mcp): 统一 MCP 工具注册与协议入口#155
HuXiaohui424 merged 14 commits into
1024XEngineer:mainfrom
HuXiaohui424:feature/mcp-skeleton

Conversation

@HuXiaohui424

Copy link
Copy Markdown
Collaborator

结论

本 PR 删除职责重复的 McpToolGateway,将工具定义、注册、参数校验、调用分发和 tools/list JSON 导出统一收敛到 McpServer

希望 Reviewer 重点判断:

  • McpServer 是否适合作为唯一工具注册和协议入口
  • 工具 Schema 与运行时参数契约是否一致
  • 参数校验是否覆盖必填项、默认值、类型、范围和多余参数
  • Runtime 对 McpServer 的依赖方向是否合理

Refs #154

变更

  • 删除 mcp_tool_gateway.h/.cc
  • 删除独立的 tool_definition.h
  • 将工具定义和 Schema 类型合并到 mcp_server.h
  • McpServer 统一负责工具注册、列表和调用分发
  • 支持布尔、整数和字符串参数
  • 增加默认值、类型、多余参数和整数范围校验
  • 增加 MCP tools/list JSON 导出
  • Runtime 改为直接依赖 McpServer
  • cJSON 调整为 MCP 私有实现依赖
  • 重写 mcp_server_test
  • 将 MCP 专项测试接入统一主机测试
  • 测试程序打印 list_tools_json(),便于检查实际协议输出

明确未包含:

  • 具体业务工具注册
  • MCP 网络传输和 JSON-RPC 请求解析
  • 浮点数、对象和数组参数
  • 运行时并发注册
  • builder/freeze 生命周期控制
  • 工具结果的 MCP JSON-RPC 编码

架构与兼容

公共接口发生变化:

  • 删除 McpToolGateway
  • 删除独立的工具定义头文件
  • 注册入口统一为 McpServer::add_tool()
  • 调用入口统一为 McpServer::call()
  • Runtime 直接持有 McpServer

依赖方向:

业务模块
    -> McpServer::add_tool()
    -> 参数 Schema 与 handler

Voice
    -> McpServer::call()

McpServer
    -> contracts
    -> cJSON(私有实现依赖)

移除硬编码的日程工具实现,新增工具注册、查询、列表和回调分发能力,并完善参数校验与中文注释
显式初始化 ToolCall 的全部字段,兼容 GCC 严格告警编译
保持组件依赖声明与仓库架构检查规则一致
限制为字符串参数,统一处理必填参数、默认值和多余参数
引入类型安全的工具参数值并在网关边界校验参数类型
原有 ToolDefinition 直接使用 input 列表,无法表达 MCP 要求的 inputSchema.type、properties 和 required 层级。\n\n新增对象根 Schema,并将字段名移入 properties、必填约束移入 required;同步调整注册校验、默认值补全和主机测试。\n\n主机 CMake 构建及 7 个测试全部通过。\n\nBREAKING CHANGE: ToolDefinition 的 input 接口迁移为 input_schema,调用方需按 JSON Schema 结构填写工具参数。
删除 McpToolGateway 和独立定义文件,由 McpServer 统一负责注册、校验、分发和 Schema 导出,并重写专项测试
合并最新 Schedule、Timing、Storage 与 CI 变更,并保留 McpServer 单入口设计
@HuXiaohui424 HuXiaohui424 added the FullSpec 规格粒度-影响面大的完整规格 label Aug 5, 2026
@HuXiaohui424 HuXiaohui424 self-assigned this Aug 5, 2026
@HuXiaohui424 HuXiaohui424 linked an issue Aug 5, 2026 that may be closed by this pull request
@HuXiaohui424 HuXiaohui424 changed the title 统一 MCP 工具注册与协议入口 ♻️ refactor(mcp): 统一 MCP 工具注册与协议入口 Aug 5, 2026

@fennoai fennoai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I found two CI-blocking issues in this diff. Details are inline.

Verification run locally against the locked PR range:

  • python3 scripts/check_public_api_docs.py fails on the new MCP public header comments.
  • cmake -S tests/host -B /tmp/voicelife-host-review fails while resolving cjson/cJSON.h.

namespace voicelife::mcp {

// MCP 工具参数支持的数据类型。
enum class ToolInputType { kString, kInteger, kBoolean };

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new public header comments are plain // comments, but the repository's public API docs gate only accepts Doxygen comments. Running python3 scripts/check_public_api_docs.py fails on this header starting here and then on the other public types/functions/accessors, so scripts/run_checks.sh will fail before tests run. Please convert the public API comments in this header to the required /// or /** ... */ form and cover the inline public functions as well.

Comment thread tests/host/CMakeLists.txt Outdated
"${ROOT_DIR}/components/voicelife_mcp/src/mcp_tool_gateway.cc")
target_link_libraries(mcp PUBLIC contracts)
"${ROOT_DIR}/components/voicelife_mcp/src/mcp_server.cc")
find_path(CJSON_INCLUDE_DIR cjson/cJSON.h PATHS /usr/local/include /opt/homebrew/include REQUIRED)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This makes the host test build require a system cJSON install, but the repo's GitHub Actions host/coverage jobs do not install libcjson-dev and the repo does not vendor a host cJSON target. In this workspace, cmake -S tests/host -B /tmp/voicelife-host-review now fails at configure with Could not find CJSON_INCLUDE_DIR, so the PR would break the existing ./scripts/run_checks.sh path. Please either provide a portable host-side dependency path/stub or update the CI/bootstrap scripts to install the dependency before making it required.

@codecov

codecov Bot commented Aug 5, 2026

Copy link
Copy Markdown

@jing-gou

jing-gou commented Aug 6, 2026

Copy link
Copy Markdown
Collaborator

@fennoai 你是资深后端/全栈架构师 + 嵌入式硬件工程师复合型专家,执行严格、客观、不留情面的代码仓库Review,请遵循下面所有评审规则,逐条输出审查结果,禁止敷衍、禁止只说空话、禁止笼统概括。
硬性约束:最终输出评审条目数量不少于20条,若直观可见问题不足,主动挖掘隐性架构隐患、软硬件兼容风险、长期运行潜在缺陷补足条目,不得简化评审内容。

评审维度

  1. 架构与模块设计
  • 模块职责是否清晰,是否存在循环依赖、职责混杂
  • 分层是否合理,是否违反单一职责原则、开闭原则
  • 接口抽象、依赖注入设计是否规范,有无硬编码耦合
  • 软件与硬件模块边界是否清晰,硬件相关逻辑是否侵入业务层
  1. 代码规范与可读性
  • 命名:变量、函数、类、文件命名是否语义清晰,禁止模糊命名、拼音命名
  • 注释:复杂逻辑、硬件时序、特殊寄存器配置必须注释;冗余注释、无效注释、过期注释需要指出
  • 代码格式、风格是否统一,是否存在大量魔法数字、魔法字符串,硬件参数无常量定义
  1. 性能隐患
  • 循环内IO、数据库重复查询、不必要的内存占用、低效算法
  • 资源是否释放(连接、句柄、定时器、文件流、硬件外设句柄)
  • 嵌入式场景:阻塞轮询、中断处理耗时过长、内存频繁分配释放
  1. 安全性检查【重点】
  • 输入校验、SQL注入、XSS、权限控制、敏感信息明文打印
  • 密钥、token、数据库地址、硬件访问口令是否硬编码提交到仓库
  • 外部指令下发至硬件驱动缺少权限校验,存在设备失控风险
  1. 健壮性 & 异常处理
  • 是否缺少异常捕获、错误分支处理
  • 参数判空、边界条件、失败重试逻辑是否完备
  • 硬件通讯异常(I2C/SPI/UART断线、设备无应答)缺少容错、恢复逻辑
  • 缺少硬件故障状态上报、故障隔离机制
  1. 硬件驱动 & 软硬件协同评审(新增专项)
  • 驱动代码与硬件原理图引脚定义是否匹配,无硬件版本兼容逻辑
  • 外设操作缺少电平保护、超时判断,存在烧毁外设芯片风险
  • 运动控制逻辑(如有)缺少软限位、急停、碰撞检测保护
  • 上下位机通讯协议:缺少校验和、重传、断线重连机制
  • 硬件参数(电流、电压、速度阈值)直接写死代码,无统一配置入口
  1. 测试覆盖
  • 是否缺少单元测试、边界用例
  • 核心业务逻辑、硬件驱动逻辑无测试直接合并风险
  • 缺少硬件异常工况、极限压力场景测试用例
  1. 工程规范
  • 无用代码、注释掉的废弃代码是否大量遗留
  • 依赖包版本风险、冗余依赖、存在已知漏洞依赖
  • 配置区分环境(开发/样机/生产),环境配置是否混写
  • 固件、上位机、3D模型、原理图版本号不统一,无关联管理
  1. 可维护性 & 扩展性
  • 新增需求是否需要大面积修改原有代码
  • 是否存在大量重复代码,缺少公共封装
  • 硬件型号迭代时,软件需要大规模改动,缺少适配抽象层

输出要求

  1. 问题分级:🔴严重阻塞(必须修改才能合并/上机测试) / 🟡建议优化(不阻塞但强烈建议重构) / 🟢可选改进
  2. 每条问题固定格式:【文件路径+代码位置】问题描述 + 风险说明 + 可直接落地的修改方案
  3. 条目数量强制≥20条;问题较少时区分表层问题与隐性长期风险,拆分为独立条目逐条列出,禁止多条问题合并为一条
  4. 最后汇总三部分:
    ① 整体仓库风险总结
    ② 优先级整改清单(阻断项优先)
    ③ 长期架构、软硬件协同优化方案
  5. 如果存在架构层面缺陷,单独提炼顶层设计问题,不要只局限单行代码
  6. 不要美化结论,发现隐患直接指出,客观评估风险等级;不使用“建议考虑”这类温和模糊表述,明确写出故障后果(程序崩溃、硬件烧毁、机构失控、数据泄露等)

现在开始对提供的仓库代码/PR变更、硬件资料、结构模型进行评审

@fennoai

fennoai Bot commented Aug 6, 2026

Copy link
Copy Markdown

Review Findings

  1. 🔴 [components/voicelife_mcp/src/mcp_server.cc:145-154] 未校验 Property.name() 为空。风险:会生成空 key 的 schema,调用层也无法稳定定位参数,后续可能出现覆盖、误解析或模型侧生成非法调用。修复:注册时直接拒绝空参数名。
  2. 🔴 [components/voicelife_mcp/src/mcp_server.cc:145-154] 未校验参数名重复。风险:unordered_map::emplace 会静默丢弃重复字段,schema 和运行时校验会脱节,最终表现为“注册成功但协议输出不一致”。修复:用 unordered_set 在注册阶段拒绝重复字段名。
  3. 🔴 [components/voicelife_mcp/include/voicelife/mcp/mcp_server.h:52-110, 66-78] 现在没有显式的 optional/required 语义,是否必填完全由“有没有默认值”决定。风险:以前能表达“可选但无默认值”的参数现在表达不了,工具契约退化。修复:给 Property 增加独立的 required 标志,并在 Schema 中保留它。
  4. 🔴 [components/voicelife_mcp/src/mcp_server.cc:145-154] 默认值只做了类型检查,没做整数范围校验。风险:默认值可以越过 min/max,调用方省略参数时会把越界值直接送进 handler,硬件阈值/控制参数会被错误放大。修复:注册阶段把默认值也跑一遍范围校验。
  5. 🟡 [components/voicelife_mcp/include/voicelife/mcp/mcp_server.h:52-110] Property 没有 description 构造入口,字段说明现在无法由业务代码注入。风险:tools/list 里描述永远空,协议可读性下降,后续接 MCP 客户端时也难以生成高质量提示。修复:把 description 放回参数声明 API。
  6. 🟡 [components/voicelife_mcp/src/mcp_server.cc:92-128] list_tools_json() 没有导出 default 值。风险:运行时会自动补默认值,但协议端看不到默认语义,客户端和模型会按“必填”去理解,行为不一致。修复:把 default 一并写进 JSON schema。
  7. 🟡 [components/voicelife_mcp/src/mcp_server.cc:92-128] JSON schema 没有显式输出 additionalProperties: false。风险:协议输出看起来允许额外字段,但运行时会拒绝,schema 与执行结果冲突。修复:在 schema 中明确禁止未声明参数。
  8. 🟠 [components/voicelife_mcp/src/mcp_server.cc:111-115] minimum / maximumdouble 写入 JSON。风险:int64_t 上界一旦超过 2^53 就会失真,范围说明会被悄悄四舍五入。修复:限制 schema 只接受安全整数范围,或改成字符串化边界值并在客户端侧明确解析。
  9. 🟡 [components/voicelife_mcp/include/voicelife/mcp/mcp_server.h:26-29, 66-79, 102-117] ToolInputSchema.propertiesunordered_map,序列化顺序不稳定。风险:tools/list 输出会抖动,快照测试、日志比对和客户端缓存都容易出现噪声。修复:保留插入顺序的同时再建索引,或者直接用有序容器。
  10. 🔴 [components/voicelife_mcp/src/mcp_server.cc:93-127] 所有 cJSON 节点都没做空指针检查。风险:低内存或分配失败时可能直接崩溃,嵌入式场景尤其危险。修复:每次 cJSON_Create* / cJSON_Add* 后立刻判空并中止构建。
  11. 🟡 [components/voicelife_mcp/src/mcp_server.cc:124-128] cJSON_Print 失败被吞成 {}。风险:真实的序列化失败会被伪装成“合法空对象”,上层无法区分协议错误和空列表。修复:返回显式错误或至少让调用方能观察到失败状态。
  12. 🟢 [components/voicelife_mcp/src/mcp_server.cc:82-89, 92-128] list_tools_json() 每次都先复制完整工具列表,再重建整棵 JSON 树。风险:工具列表一旦频繁查询,会产生额外堆分配和碎片。修复:直接在注册顺序上流式生成,或者缓存序列化结果。
  13. 🟡 [components/voicelife_mcp/src/mcp_server.cc:167-206] 必填参数检查对每个字段都做一次线性 std::find。风险:字段一多就变成 O(n^2),在设备侧没有必要。修复:把 required 改成集合或在字段上带布尔位。
  14. 🟢 [components/voicelife_mcp/src/mcp_server.cc:131-134] PropertyList::with_values() 先复制整张参数表再交给 handler。风险:每次调用都多一次 map 拷贝,纯粹增加内存 churn。修复:把已归一化后的 ToolCall 直接传给 handler,或让 PropertyList 持有轻量视图。
  15. 🟡 [components/voicelife_mcp/include/voicelife/mcp/mcp_server.h:183-229] 现在没有 builder/freeze 生命周期控制,也没有并发保护。风险:一旦后续把注册和调用放到不同线程,registry 会出现数据竞争;即便单线程,晚注册也会让运行时状态难以推理。修复:拆成构建期注册器和运行期只读服务器,或加 mutex 并明确冻结点。
  16. 🟡 [components/voicelife_mcp/test/CMakeLists.txt:1-32] 这个 standalone test 工程没有被根构建或 CI 引用。风险:文件存在,但实际不会跑,等于给人一个假的测试覆盖印象。修复:要么接入根 CMake/CI,要么删掉这份死代码。
  17. 🟡 [components/voicelife_mcp/test/CMakeLists.txt:24-32, tests/host/CMakeLists.txt:95-97] 两边都定义了 mcp_server_test 同名目标。风险:一旦这两个构建路径同时被启用,CMake 会直接目标冲突。修复:统一保留一个入口,或改成唯一命名空间前缀。
  18. 🟡 [tests/host/CMakeLists.txt:95-97, components/voicelife_mcp/test/mcp_server_test.cc:60-83] 新增测试没有覆盖空参数名、重复参数名、默认值越界这些本次最危险的分支。风险:这次重构最容易回归的地方没有被锁住。修复:补一组负例测试,直接打这些边界条件。
  19. 🟡 [components/voicelife_mcp/test/mcp_server_test.cc:160-166] JSON 校验只做了 substring 搜索。风险:格式错误、字段层级错误、数组结构错误都可能误判通过。修复:把输出喂给 cJSON 解析器,逐层断言 tools / inputSchema / properties / required
  20. 🟡 [tests/host/runtime_smoke_test.cc:43-47] runtime smoke 只验证“未知工具会被拒绝”,没有覆盖真实注册和分发链路。风险:add_tool()、参数归一化、call() 这条链一旦坏掉,现有 runtime smoke 不会报警。修复:在 smoke 测试里注册一个最小工具并实际调用。
  21. 🔴 [tests/host/CMakeLists.txt:44-49, 95-97] 我本地跑 ./scripts/run_host_tests.sh -R mcp_server_test,CMake 配置阶段直接卡在找不到 cJSON.h。风险:干净环境下主机测试链路不可用,贡献者必须手工装系统包才能构建。修复:把依赖 bootstrap 写进脚本/文档,或者改成可获取的包管理方案。
  22. 🟡 [components/voicelife_mcp/idf_component.yml:1-2, tests/host/CMakeLists.txt:44-49, .github/workflows/ci.yml:92-98] cJSON 的来源分裂成 IDF 依赖、宿主机系统包和 CI apt 安装三套链。风险:三个环境很容易版本漂移,主机测试、固件构建和 CI 结果不再代表同一份依赖。修复:统一依赖获取策略,至少把 host 与 IDF 的版本约束写成同一份真源。

整体仓库风险总结

这次重构方向是对的,但当前实现把“协议 schema、运行时参数契约、序列化、构建依赖”一起改了,最危险的是契约退化和 host 构建不可用。现在最直接的故障面不是业务逻辑本身,而是工具定义会失真、默认值会越界、空环境下连测试都起不来。

优先级整改清单

  1. 先补回参数契约:空名、重名、optional/required、默认值范围校验。
  2. 再补协议输出:default、description、additionalProperties: false、结构化 JSON 断言。
  3. 然后修 host 构建:把 cJSON 依赖获取统一起来,保证 run_host_tests.sh 在干净环境可跑。
  4. 最后处理架构收敛:builder/freeze、并发边界、测试接入和死 CMake 文件清理。

长期架构、软硬件协同优化方案

McpServer 拆成“注册模型 + 只读运行时 + 序列化器”三层,注册期负责校验和冻结,运行期只读调用,序列化器只做协议输出。这样既能减少后续硬件参数扩散,也能把电流、电压、速度等安全阈值统一收口到可测试的 schema 层,避免把控制面逻辑散落在 handler 里。

验证:我还尝试了 ./scripts/run_host_tests.sh -R mcp_server_test,当前仓库在本机直接因缺少 cJSON.h 失败,说明 host 依赖并没有被脚本完整自举。

@JunLang-7

Copy link
Copy Markdown
Collaborator

@fennoai 当前实现使用了cJSON 但是cJSON会接受 RFC 8259 禁止的前导零 01 你觉得这个问题需要注意吗

@JunLang-7

JunLang-7 commented Aug 6, 2026

Copy link
Copy Markdown
Collaborator

当前PR不会复现 #149 里最核心的“宽松解析器导致双端拒绝语义漂移”问题,因为它当前没有用 cJSON 解析输入。

PR 中 cJSON 只出现在:

  • mcp_server.cc#L92-L128
  • 用于生成 tools/list JSON
  • tools/call JSON 报文解析明确列为非目标
  • 仓库中没有 cJSON_Parse() 调用

因此,011e999、控制字符、非法 UTF-8、重复键等“输入解析”问题暂时不会进入这条代码路径。重复键也不是当前问题:输出对象来自 unordered_map,不是 cJSON 解析外部 JSON。

但当前序列化实现仍有几个类似的契约风险。

需要修正的问题

  1. int64_t 的 Schema 边界被错误转换成 double

mcp_server.cc#L111-L115

cJSON_AddNumberToObject(property, "minimum",
                        static_cast<double>(*field.minimum));

但公共 API 支持完整的 int64_t

mcp_server.h#L21-L27

我用 cJSON 1.7.19 实测:

9007199254740993
=> 9.00719925474099e+15

INT64_MAX
=> 9.2233720368547758e+18

这会造成导出的 JSON Schema 与设备实际的整数校验范围不一致。尤其是 2^53 以上的整数在 JavaScript number 中也无法精确表示。

解决方向只有两个:

  • 使用支持有符号 64 位整数的 JSON writer,例如 yyjson 的 yyjson_mut_obj_add_sint()
  • 或者明确把 MCP 整数范围限制在 [-2^53+1, 2^53-1],并在注册时拒绝超出范围的 Schema。
  1. std::string 经过 c_str() 后会发生 NUL 截断,非法 UTF-8 也不会被拒绝

mcp_server.cc#L97-L120 多处把 std::string 转成 C 字符串:

cJSON_AddStringToObject(tool, "name", definition.name.c_str());

实测:

std::string("a\0b", 3)
=> {"name":"a"}

所以 list_tools() 中的工具名和 list_tools_json() 中的工具名可能不一致。

另外,非法 UTF-8 字节会被 cJSON 原样写入输出。例如 C3 28 会直接出现在 JSON 字符串中,产生不符合 RFC 8259 UTF-8 要求的输出。

如果工具定义被认为是可信的编译期数据,这个风险较低;但当前 API 没有声明或校验这个前提。建议至少:

  • 注册时拒绝名称、描述、属性名中的 '\0'
  • 校验 UTF-8;
  • 或改用支持显式长度的字符串 API,例如 yyjson 的 *_strn()

这和 #149 中“内嵌 NUL 不应丢失”的测试关注点是同一类边界问题,只不过这里发生在序列化端。

  1. 所有 cJSON 分配失败都被吞掉

mcp_server.cc#L93-L127 没有检查 CreateObjectAddObjectAddArrayAddString 等返回值。

如果内存不足,cJSON_Print() 失败后函数返回 "{}"。这虽然是合法 JSON,但不再满足函数文档所说的“包含 tools 数组”,调用方也无法区分“空工具列表”和“序列化失败”。

对于 ESP32,建议让接口返回带错误状态的结果,或者至少检查整棵 cJSON 树和 cJSON_Print() 的失败路径。

关于“IDF 自带 json”

这里需要区分 ESP-IDF 版本:

  • ESP-IDF 5.5 仍有内置 json 组件;
  • ESP-IDF 6.0 已移除内置 json,官方迁移方式就是 espressif/cjson
  • 当前PR的 CI 目标是 ESP-IDF 6.0.2,CI 日志也明确显示:
Processing 2 dependencies:
[1/2] espressif/cjson (1.7.19)

所以当前PR使用:

PRIV_REQUIRES espressif__cjson

以及 idf_component.yml 是符合 IDF 6 官方迁移方式的。不能简单改成 PRIV_REQUIRES json

我的判断

我不会因为“cJSON 宽松解析”这个理由否掉当前PR;当前 PR 是输出-only,和 #149 的输入解析场景不同。

但我不会让它按当前实现直接合入,至少应补:

  • 大于 2^53 的整数边界测试;
  • 内嵌 NUL 和非法 UTF-8 测试;
  • cJSON 分配失败处理;
  • 明确未来 tools/call 解析不得直接复用 cJSON。

另外,#149 已经把 yyjson 放进当前 main。如果当前PR合并,固件里会同时存在 yyjson 和 cJSON 两套 JSON 实现。若 MCP 后续还会增加 JSON-RPC 解析,我更倾向于统一使用 yyjson;如果只需要这个很小的 tools/list 序列化器,cJSON 仍然是合理选择,但必须补上上述边界约束。

@fennoai

fennoai Bot commented Aug 6, 2026

Copy link
Copy Markdown

结论

要注意,但就当前 PR 的代码路径来说,它不是阻塞项

现在 cJSON 只用于 tools/list 这类序列化输出,前导零 01 这类问题不会从这里冒出来。真正需要警惕的是:如果后续把 cJSON 用到外部 JSON-RPC 请求解析,它的宽松解析会让不符合 RFC 8259 的数值字面量通过,协议兼容性会直接出问题。

所以我的判断是:

  • 当前实现:不构成直接缺陷
  • 未来解析外部输入:必须处理,不能放过

对应修复方向只有一个:外部输入边界用严格 JSON 解析/校验,不要把 cJSON 的宽松容错当成协议正确性。

# Conflicts:
#	components/voicelife_runtime/src/runtime.cc
@HuXiaohui424

HuXiaohui424 commented Aug 7, 2026

Copy link
Copy Markdown
Collaborator Author

当前PR不会复现 #149 里最核心的“宽松解析器导致双端拒绝语义漂移”问题,因为它当前没有用 cJSON 解析输入。

PR 中 cJSON 只出现在:

  • mcp_server.cc#L92-L128
  • 用于生成 tools/list JSON
  • tools/call JSON 报文解析明确列为非目标
  • 仓库中没有 cJSON_Parse() 调用

因此,011e999、控制字符、非法 UTF-8、重复键等“输入解析”问题暂时不会进入这条代码路径。重复键也不是当前问题:输出对象来自 unordered_map,不是 cJSON 解析外部 JSON。

但当前序列化实现仍有几个类似的契约风险。

需要修正的问题

  1. int64_t 的 Schema 边界被错误转换成 double

mcp_server.cc#L111-L115

cJSON_AddNumberToObject(property, "minimum",
                        static_cast<double>(*field.minimum));

但公共 API 支持完整的 int64_t

mcp_server.h#L21-L27

我用 cJSON 1.7.19 实测:

9007199254740993
=> 9.00719925474099e+15

INT64_MAX
=> 9.2233720368547758e+18

这会造成导出的 JSON Schema 与设备实际的整数校验范围不一致。尤其是 2^53 以上的整数在 JavaScript number 中也无法精确表示。

解决方向只有两个:

  • 使用支持有符号 64 位整数的 JSON writer,例如 yyjson 的 yyjson_mut_obj_add_sint()
  • 或者明确把 MCP 整数范围限制在 [-2^53+1, 2^53-1],并在注册时拒绝超出范围的 Schema。
  1. std::string 经过 c_str() 后会发生 NUL 截断,非法 UTF-8 也不会被拒绝

mcp_server.cc#L97-L120 多处把 std::string 转成 C 字符串:

cJSON_AddStringToObject(tool, "name", definition.name.c_str());

实测:

std::string("a\0b", 3)
=> {"name":"a"}

所以 list_tools() 中的工具名和 list_tools_json() 中的工具名可能不一致。

另外,非法 UTF-8 字节会被 cJSON 原样写入输出。例如 C3 28 会直接出现在 JSON 字符串中,产生不符合 RFC 8259 UTF-8 要求的输出。

如果工具定义被认为是可信的编译期数据,这个风险较低;但当前 API 没有声明或校验这个前提。建议至少:

  • 注册时拒绝名称、描述、属性名中的 '\0'
  • 校验 UTF-8;
  • 或改用支持显式长度的字符串 API,例如 yyjson 的 *_strn()

这和 #149 中“内嵌 NUL 不应丢失”的测试关注点是同一类边界问题,只不过这里发生在序列化端。

  1. 所有 cJSON 分配失败都被吞掉

mcp_server.cc#L93-L127 没有检查 CreateObjectAddObjectAddArrayAddString 等返回值。

如果内存不足,cJSON_Print() 失败后函数返回 "{}"。这虽然是合法 JSON,但不再满足函数文档所说的“包含 tools 数组”,调用方也无法区分“空工具列表”和“序列化失败”。

对于 ESP32,建议让接口返回带错误状态的结果,或者至少检查整棵 cJSON 树和 cJSON_Print() 的失败路径。

关于“IDF 自带 json”

这里需要区分 ESP-IDF 版本:

  • ESP-IDF 5.5 仍有内置 json 组件;
  • ESP-IDF 6.0 已移除内置 json,官方迁移方式就是 espressif/cjson
  • 当前PR的 CI 目标是 ESP-IDF 6.0.2,CI 日志也明确显示:
Processing 2 dependencies:
[1/2] espressif/cjson (1.7.19)

所以当前PR使用:

PRIV_REQUIRES espressif__cjson

以及 idf_component.yml 是符合 IDF 6 官方迁移方式的。不能简单改成 PRIV_REQUIRES json

我的判断

我不会因为“cJSON 宽松解析”这个理由否掉当前PR;当前 PR 是输出-only,和 #149 的输入解析场景不同。

但我不会让它按当前实现直接合入,至少应补:

  • 大于 2^53 的整数边界测试;
  • 内嵌 NUL 和非法 UTF-8 测试;
  • cJSON 分配失败处理;
  • 明确未来 tools/call 解析不得直接复用 cJSON。

另外,#149 已经把 yyjson 放进当前 main。如果当前PR合并,固件里会同时存在 yyjson 和 cJSON 两套 JSON 实现。若 MCP 后续还会增加 JSON-RPC 解析,我更倾向于统一使用 yyjson;如果只需要这个很小的 tools/list 序列化器,cJSON 仍然是合理选择,但必须补上上述边界约束。

关于范围问题,只有-9,007,199,254,740,991 ~ 9,007,199,254,740,991在这个范围之外才会出现问题,这个范围已经足够大,满足我们的业务需求。
其次,在对接灵矽平台序列化/反序列化JSON 时也是使用的 cJSON,详情可咨询 @ZhaoXingPeng

不过为了最终统一,我决定改为 yyJSON

@JunLang-7

Copy link
Copy Markdown
Collaborator

@fennoai 你是资深后端/全栈架构师 + 嵌入式硬件工程师复合型专家,执行严格、客观、不留情面的代码仓库Review,请遵循下面所有评审规则,逐条输出审查结果,禁止敷衍、禁止只说空话、禁止笼统概括。
硬性约束:最终输出评审条目数量不少于20条,若直观可见问题不足,主动挖掘隐性架构隐患、软硬件兼容风险、长期运行潜在缺陷补足条目,不得简化评审内容。

评审维度

  1. 架构与模块设计
  • 模块职责是否清晰,是否存在循环依赖、职责混杂
  • 分层是否合理,是否违反单一职责原则、开闭原则
  • 接口抽象、依赖注入设计是否规范,有无硬编码耦合
  • 软件与硬件模块边界是否清晰,硬件相关逻辑是否侵入业务层
  1. 代码规范与可读性
  • 命名:变量、函数、类、文件命名是否语义清晰,禁止模糊命名、拼音命名
  • 注释:复杂逻辑、硬件时序、特殊寄存器配置必须注释;冗余注释、无效注释、过期注释需要指出
  • 代码格式、风格是否统一,是否存在大量魔法数字、魔法字符串,硬件参数无常量定义
  1. 性能隐患
  • 循环内IO、数据库重复查询、不必要的内存占用、低效算法
  • 资源是否释放(连接、句柄、定时器、文件流、硬件外设句柄)
  • 嵌入式场景:阻塞轮询、中断处理耗时过长、内存频繁分配释放
  1. 安全性检查【重点】
  • 输入校验、SQL注入、XSS、权限控制、敏感信息明文打印
  • 密钥、token、数据库地址、硬件访问口令是否硬编码提交到仓库
  • 外部指令下发至硬件驱动缺少权限校验,存在设备失控风险
  1. 健壮性 & 异常处理
  • 是否缺少异常捕获、错误分支处理
  • 参数判空、边界条件、失败重试逻辑是否完备
  • 硬件通讯异常(I2C/SPI/UART断线、设备无应答)缺少容错、恢复逻辑
  • 缺少硬件故障状态上报、故障隔离机制
  1. 硬件驱动 & 软硬件协同评审(新增专项)
  • 驱动代码与硬件原理图引脚定义是否匹配,无硬件版本兼容逻辑
  • 外设操作缺少电平保护、超时判断,存在烧毁外设芯片风险
  • 运动控制逻辑(如有)缺少软限位、急停、碰撞检测保护
  • 上下位机通讯协议:缺少校验和、重传、断线重连机制
  • 硬件参数(电流、电压、速度阈值)直接写死代码,无统一配置入口
  1. 测试覆盖
  • 是否缺少单元测试、边界用例
  • 核心业务逻辑、硬件驱动逻辑无测试直接合并风险
  • 缺少硬件异常工况、极限压力场景测试用例
  1. 工程规范
  • 无用代码、注释掉的废弃代码是否大量遗留
  • 依赖包版本风险、冗余依赖、存在已知漏洞依赖
  • 配置区分环境(开发/样机/生产),环境配置是否混写
  • 固件、上位机、3D模型、原理图版本号不统一,无关联管理
  1. 可维护性 & 扩展性
  • 新增需求是否需要大面积修改原有代码
  • 是否存在大量重复代码,缺少公共封装
  • 硬件型号迭代时,软件需要大规模改动,缺少适配抽象层

输出要求

  1. 问题分级:🔴严重阻塞(必须修改才能合并/上机测试) / 🟡建议优化(不阻塞但强烈建议重构) / 🟢可选改进
  2. 每条问题固定格式:【文件路径+代码位置】问题描述 + 风险说明 + 可直接落地的修改方案
  3. 条目数量强制≥20条;问题较少时区分表层问题与隐性长期风险,拆分为独立条目逐条列出,禁止多条问题合并为一条
  4. 最后汇总三部分:
    ① 整体仓库风险总结
    ② 优先级整改清单(阻断项优先)
    ③ 长期架构、软硬件协同优化方案
  5. 如果存在架构层面缺陷,单独提炼顶层设计问题,不要只局限单行代码
  6. 不要美化结论,发现隐患直接指出,客观评估风险等级;不使用“建议考虑”这类温和模糊表述,明确写出故障后果(程序崩溃、硬件烧毁、机构失控、数据泄露等)

现在开始对提供的仓库代码/PR变更、硬件资料、结构模型进行评审

@fennoai

fennoai Bot commented Aug 7, 2026

Copy link
Copy Markdown

Review Complete

  • Lock PR workspace and fixed SHAs
  • Read current repo context
  • Finalize review findings
  • Submit PR review

已提交一条 COMMENT 类型 PR review,重点集中在参数契约、schema/runtime 一致性、JSON 序列化失败处理和测试覆盖缺口。

View job run

@fennoai fennoai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review Findings

  1. 🔴 [components/voicelife_mcp/src/mcp_server.cc:66-79] Empty property names are not rejected. Risk: a tool can register an unnamed parameter, which produces a schema key clients cannot address reliably. Fix: reject property.name().empty() during registration.
  2. 🔴 [components/voicelife_mcp/src/mcp_server.cc:66-79] Duplicate property names are silently collapsed by unordered_map::emplace. Risk: schema and runtime contracts diverge, and the later property never reaches tools/list. Fix: reject duplicate names before building the schema.
  3. 🔴 [components/voicelife_mcp/src/mcp_server.cc:100-123] Default values are only type-checked, not range-checked. Risk: an out-of-range default is accepted and later injected into handler calls when the caller omits the field, which can push unsafe values downstream. Fix: validate defaults against the same min/max gate used at call time.
  4. 🟡 [components/voicelife_mcp/include/voicelife/mcp/mcp_server.h:52-77] Property has no way to set description. Risk: the schema serializer will always emit blank parameter docs, so generated MCP prompts lose context. Fix: add a description argument or setter and propagate it into ToolInputField.
  5. 🟡 [components/voicelife_mcp/include/voicelife/mcp/mcp_server.h:52-77] Required-vs-optional semantics are inferred solely from the presence of a default. Risk: you cannot express “optional but no default”, so the public API is narrower than the protocol contract. Fix: add an explicit required flag to Property and serialize it directly.
  6. 🔴 [components/voicelife_mcp/src/mcp_json_writer.cc:118-153] SerializeListToolsResult() never emits default. Risk: tools/list tells clients a field is required even when runtime will auto-fill it, so model-facing contract and execution path diverge. Fix: serialize default alongside type and description.
  7. 🔴 [components/voicelife_mcp/src/mcp_json_writer.cc:118-153] The schema never emits additionalProperties: false. Risk: the JSON schema suggests extra fields are allowed while McpServer::call() rejects them, causing protocol/client mismatch. Fix: write additionalProperties: false into every tool schema.
  8. 🟡 [components/voicelife_mcp/src/mcp_json_writer.cc:134-153] Property iteration uses unordered_map, so JSON key order changes across runs. Risk: snapshot tests, diff-based audits, and client caches see noisy churn. Fix: preserve insertion order in the schema model or sort keys before serialization.
  9. 🔴 [components/voicelife_mcp/src/mcp_json_writer.cc:158-183] Any allocation or append failure collapses to {}. Risk: OOM or serializer bugs are silently converted into a valid-looking empty object, making protocol failures indistinguishable from “no tools”. Fix: surface an error status or at least a distinguishable sentinel.
  10. 🟡 [components/voicelife_mcp/src/mcp_server.cc:82-92] list_tools_json() materializes a full ListToolsResult and then serializes it. Risk: every list call allocates and copies the full registry twice. Fix: serialize directly from the registry or cache the rendered JSON.
  11. 🟢 [components/voicelife_mcp/src/mcp_server.cc:82-89] list_tools() copies every ToolDefinition into a new vector on each call. Risk: repeated list calls create unnecessary heap churn. Fix: return a view/cached snapshot or store definitions in a stable container.
  12. 🟡 [components/voicelife_mcp/src/mcp_server.cc:137-183] McpServer has no freeze or lock boundary. Risk: if registration and dispatch ever become concurrent, the registry becomes data-race prone and late registration changes live behavior. Fix: split build-time registration from read-only dispatch or guard the registry with a mutex and explicit freeze.
  13. 🟢 [components/voicelife_mcp/src/mcp_server.cc:149-160] Required-parameter lookup does a linear std::find for every missing field. Risk: the check becomes quadratic as tool signatures grow. Fix: store required names in a set or a boolean bit on each field.
  14. 🟢 [components/voicelife_mcp/src/mcp_server.cc:149-151] defined_names is rebuilt as an unordered_set on every call. Risk: each dispatch allocates extra memory and hashes every parameter name again. Fix: precompute the lookup structure at registration time.
  15. 🟢 [components/voicelife_mcp/src/mcp_server.cc:94-97] PropertyList::with_values() copies the whole arguments map before handing it to the callback. Risk: every call pays an extra map copy even when handlers only read one value. Fix: pass the normalized ToolCall directly or bind values by view.
  16. 🟡 [components/voicelife_mcp/test/mcp_server_test.cc:63-85] Registration validation tests miss the two most dangerous new failures: empty parameter names and duplicate names. Risk: the contract bugs above can regress silently. Fix: add explicit negative cases for both.
  17. 🟡 [components/voicelife_mcp/test/mcp_server_test.cc:155-207] The JSON assertions only check a happy path and one integer-boundary tool. Risk: regressions in default, required, additionalProperties, or property ordering will still pass. Fix: parse and assert those fields explicitly.
  18. 🟡 [components/voicelife_mcp/test/mcp_server_test.cc:74-85] There is no test that an integer default outside min/max is rejected. Risk: the runtime can accept a tool definition that later injects unsafe values on omitted input. Fix: add a negative registration case for out-of-range defaults.
  19. 🟡 [tests/host/runtime_smoke_test.cc:36-48] The smoke test still never registers a real tool before dispatch. Risk: the new McpServer bridge path can break without the integration test noticing. Fix: register a trivial tool and call it through VoiceSessionCoordinator.
  20. 🟡 [components/voicelife_mcp/test/CMakeLists.txt:1-35] The standalone component test project is not referenced by the root build or CI. Risk: it can rot while looking like coverage. Fix: either wire it into the build or remove it.
  21. 🟡 [components/voicelife_mcp/test/CMakeLists.txt:23-35, tests/host/CMakeLists.txt:178-181] The same mcp_server_test is defined in two CMake entry points. Risk: future umbrella builds will hit target collisions or diverging test logic. Fix: keep one authoritative test target and delete the duplicate project.

Overall Risk

This refactor moves the right direction, but the current implementation still leaves contract drift between tool registration, schema export, and runtime dispatch. The biggest practical risks are invalid tool definitions slipping through, protocol output that does not describe runtime behavior accurately, and test coverage that still misses the new failure modes.

Priority Fixes

  1. Block invalid tool definitions first: empty names, duplicate names, and out-of-range defaults.
  2. Align schema output with runtime: default, required, and additionalProperties must match call-time behavior.
  3. Remove silent failure paths in JSON serialization.
  4. Tighten coverage on registration negatives and bridge-path integration.

Long-term

Split the MCP surface into a build-time registration model and a read-only runtime snapshot. That gives you one place for schema validation, one place for serialization, and a clear boundary for future concurrent registration or hardware-safety policy.

验证:./scripts/run_host_tests.sh -R mcp_server_test 已通过。

@HuXiaohui424

Copy link
Copy Markdown
Collaborator Author

Review Findings

  1. 🔴 [components/voicelife_mcp/src/mcp_server.cc:66-79] Empty property names are not rejected. Risk: a tool can register an unnamed parameter, which produces a schema key clients cannot address reliably. Fix: reject property.name().empty() during registration.
  2. 🔴 [components/voicelife_mcp/src/mcp_server.cc:66-79] Duplicate property names are silently collapsed by unordered_map::emplace. Risk: schema and runtime contracts diverge, and the later property never reaches tools/list. Fix: reject duplicate names before building the schema.
  3. 🔴 [components/voicelife_mcp/src/mcp_server.cc:100-123] Default values are only type-checked, not range-checked. Risk: an out-of-range default is accepted and later injected into handler calls when the caller omits the field, which can push unsafe values downstream. Fix: validate defaults against the same min/max gate used at call time.
  4. 🟡 [components/voicelife_mcp/include/voicelife/mcp/mcp_server.h:52-77] Property has no way to set description. Risk: the schema serializer will always emit blank parameter docs, so generated MCP prompts lose context. Fix: add a description argument or setter and propagate it into ToolInputField.
  5. 🟡 [components/voicelife_mcp/include/voicelife/mcp/mcp_server.h:52-77] Required-vs-optional semantics are inferred solely from the presence of a default. Risk: you cannot express “optional but no default”, so the public API is narrower than the protocol contract. Fix: add an explicit required flag to Property and serialize it directly.
  6. 🔴 [components/voicelife_mcp/src/mcp_json_writer.cc:118-153] SerializeListToolsResult() never emits default. Risk: tools/list tells clients a field is required even when runtime will auto-fill it, so model-facing contract and execution path diverge. Fix: serialize default alongside type and description.
  7. 🔴 [components/voicelife_mcp/src/mcp_json_writer.cc:118-153] The schema never emits additionalProperties: false. Risk: the JSON schema suggests extra fields are allowed while McpServer::call() rejects them, causing protocol/client mismatch. Fix: write additionalProperties: false into every tool schema.
  8. 🟡 [components/voicelife_mcp/src/mcp_json_writer.cc:134-153] Property iteration uses unordered_map, so JSON key order changes across runs. Risk: snapshot tests, diff-based audits, and client caches see noisy churn. Fix: preserve insertion order in the schema model or sort keys before serialization.
  9. 🔴 [components/voicelife_mcp/src/mcp_json_writer.cc:158-183] Any allocation or append failure collapses to {}. Risk: OOM or serializer bugs are silently converted into a valid-looking empty object, making protocol failures indistinguishable from “no tools”. Fix: surface an error status or at least a distinguishable sentinel.
  10. 🟡 [components/voicelife_mcp/src/mcp_server.cc:82-92] list_tools_json() materializes a full ListToolsResult and then serializes it. Risk: every list call allocates and copies the full registry twice. Fix: serialize directly from the registry or cache the rendered JSON.
  11. 🟢 [components/voicelife_mcp/src/mcp_server.cc:82-89] list_tools() copies every ToolDefinition into a new vector on each call. Risk: repeated list calls create unnecessary heap churn. Fix: return a view/cached snapshot or store definitions in a stable container.
  12. 🟡 [components/voicelife_mcp/src/mcp_server.cc:137-183] McpServer has no freeze or lock boundary. Risk: if registration and dispatch ever become concurrent, the registry becomes data-race prone and late registration changes live behavior. Fix: split build-time registration from read-only dispatch or guard the registry with a mutex and explicit freeze.
  13. 🟢 [components/voicelife_mcp/src/mcp_server.cc:149-160] Required-parameter lookup does a linear std::find for every missing field. Risk: the check becomes quadratic as tool signatures grow. Fix: store required names in a set or a boolean bit on each field.
  14. 🟢 [components/voicelife_mcp/src/mcp_server.cc:149-151] defined_names is rebuilt as an unordered_set on every call. Risk: each dispatch allocates extra memory and hashes every parameter name again. Fix: precompute the lookup structure at registration time.
  15. 🟢 [components/voicelife_mcp/src/mcp_server.cc:94-97] PropertyList::with_values() copies the whole arguments map before handing it to the callback. Risk: every call pays an extra map copy even when handlers only read one value. Fix: pass the normalized ToolCall directly or bind values by view.
  16. 🟡 [components/voicelife_mcp/test/mcp_server_test.cc:63-85] Registration validation tests miss the two most dangerous new failures: empty parameter names and duplicate names. Risk: the contract bugs above can regress silently. Fix: add explicit negative cases for both.
  17. 🟡 [components/voicelife_mcp/test/mcp_server_test.cc:155-207] The JSON assertions only check a happy path and one integer-boundary tool. Risk: regressions in default, required, additionalProperties, or property ordering will still pass. Fix: parse and assert those fields explicitly.
  18. 🟡 [components/voicelife_mcp/test/mcp_server_test.cc:74-85] There is no test that an integer default outside min/max is rejected. Risk: the runtime can accept a tool definition that later injects unsafe values on omitted input. Fix: add a negative registration case for out-of-range defaults.
  19. 🟡 [tests/host/runtime_smoke_test.cc:36-48] The smoke test still never registers a real tool before dispatch. Risk: the new McpServer bridge path can break without the integration test noticing. Fix: register a trivial tool and call it through VoiceSessionCoordinator.
  20. 🟡 [components/voicelife_mcp/test/CMakeLists.txt:1-35] The standalone component test project is not referenced by the root build or CI. Risk: it can rot while looking like coverage. Fix: either wire it into the build or remove it.
  21. 🟡 [components/voicelife_mcp/test/CMakeLists.txt:23-35, tests/host/CMakeLists.txt:178-181] The same mcp_server_test is defined in two CMake entry points. Risk: future umbrella builds will hit target collisions or diverging test logic. Fix: keep one authoritative test target and delete the duplicate project.

Overall Risk

This refactor moves the right direction, but the current implementation still leaves contract drift between tool registration, schema export, and runtime dispatch. The biggest practical risks are invalid tool definitions slipping through, protocol output that does not describe runtime behavior accurately, and test coverage that still misses the new failure modes.

Priority Fixes

  1. Block invalid tool definitions first: empty names, duplicate names, and out-of-range defaults.
  2. Align schema output with runtime: default, required, and additionalProperties must match call-time behavior.
  3. Remove silent failure paths in JSON serialization.
  4. Tighten coverage on registration negatives and bridge-path integration.

Long-term

Split the MCP surface into a build-time registration model and a read-only runtime snapshot. That gives you one place for schema validation, one place for serialization, and a clear boundary for future concurrent registration or hardware-safety policy.

验证:./scripts/run_host_tests.sh -R mcp_server_test 已通过。

@fennoai response in Chinese

@fennoai

fennoai Bot commented Aug 7, 2026

Copy link
Copy Markdown

中文回复

这组评审结论是成立的,重点问题没有偏离:

  1. 参数契约不完整,空名、重名和默认值越界都可能让工具定义和实际调用脱节。
  2. tools/list 的 schema 目前还没有和运行时行为完全对齐,尤其是 defaultrequiredadditionalProperties 这几项。
  3. 序列化失败被吞成 {},会把真实错误伪装成正常空结果。
  4. 测试覆盖还没锁住最危险的负例和桥接链路。

结论保持不变:这些属于应优先修复的阻断项,先修契约和 schema 一致性,再补失败路径和测试覆盖。

@HuXiaohui424
HuXiaohui424 merged commit 0d71f48 into 1024XEngineer:main Aug 7, 2026
13 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

FullSpec 规格粒度-影响面大的完整规格

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[MCP] 统一 MCP 工具注册入口并完善协议 Schema

3 participants