让小智真的动起来:用 MCP 给它注册一个能调用的动作
- 理解 MCP 在小智里承担的角色:把设备动作暴露成可被调用的工具
- 会用 AddTool 注册无参数和带参数两类动作
- 知道工具名和描述该怎么写,才能让模型正确地调它
- 明白为什么新项目应该统一用 MCP 而不是自己发明控制协议
本文讲解原理与流程、引用关键片段并注明出处,版权归原作者,遵循其开源协议;一切以上游仓库最新版本为准。
小智能听会说之后,多数人的下一个念头是同一个:能不能让它控制点什么。
开个灯、转个舵机、让小车往前走——这一步才是"AI 硬件"和"智能音箱"的分界线。小智给这件事准备的机制叫 MCP。
本篇的代码片段引自上游 mcp-usage_zh.md,用于讲解注册方式,具体接口以上游文档和你所用版本的代码为准。代码版权归原作者 78 及社区贡献者,遵循 MIT 协议。本文写于 2026 年 7 月底。
MCP 在这里是干什么的
一句话:通过标准的 JSON-RPC 2.0 格式,在后台与设备之间发现和调用「工具」(Tool),实现灵活的设备控制。
拆开看,关键词有三个:
「发现」 —— 后台不需要预先知道你这台设备能干什么。设备上线时把自己的工具报上去,后台就知道了。这意味着你加一个新动作,不用改后台。
「工具」 —— 每个设备动作被包装成一个有名字、有描述、有参数的工具。这套抽象和现在大模型调外部能力的方式是一致的,不是小智自创的概念。
「JSON-RPC 2.0」 —— 用的是成熟的标准协议,不是私有格式。
理解这套设计的价值,要从大模型的角度看:它不知道你的硬件长什么样,它只看到一个工具清单——名字、说明、参数。它根据用户说的话,判断该调哪个、传什么参数。所以"注册工具"这件事,本质上是在给模型写一份能力说明书。
注册一个无参数的动作
最简单的形式,来自上游文档的例子:
mcp_server.AddTool("self.dog.forward", "机器人向前移动",
PropertyList(), [this](const PropertyList&) -> ReturnValue {
servo_dog_ctrl_send(DOG_STATE_FORWARD, NULL);
return true;
});
四个部分看明白就够了:
"self.dog.forward"—— 工具名。注意这个层级式的命名习惯:self表示这台设备自己,后面是设备.动作。名字取得清楚,模型才容易选对。"机器人向前移动"—— 描述。这句话是给模型看的,下面单独讲。PropertyList()—— 参数列表,这里是空的,表示不需要参数。- lambda —— 真正干活的地方。这个例子里就是发一个控制指令给舵机,然后返回
true。
结构非常清爽:注册 = 名字 + 描述 + 参数 + 干什么。 你已有的硬件控制函数(点灯、转舵机、发指令)原样调用就行,MCP 只是给它包了层壳。
注册一个带参数的动作
mcp_server.AddTool("self.light.set_rgb", "设置RGB颜色",
PropertyList({Property("r", kPropertyTypeInteger, 0, 255), ...}),
[this](const PropertyList& properties) -> ReturnValue {
// 参数处理和设备控制逻辑
});
区别只在第三个参数:PropertyList 里声明了这个工具接受什么参数。上面这行声明了一个名为 r 的整数参数,取值范围 0 到 255。
参数支持的类型是布尔、整数、字符串,可以指定范围和默认值;返回值支持 bool、int、string。
范围(0–255)这个细节值得注意:它不只是运行时的校验,更是告诉模型"这个参数该给什么样的值"。声明了范围,模型传出格外离谱的数值的概率会显著降低。能声明范围就声明,这是低成本高收益的事。
描述怎么写,比代码怎么写更关键 ⭐
这是全篇最想强调的一点,也是从软件转过来的人最容易轻视的地方。
写普通函数时,注释写不写、写多好,不影响它能不能被正确调用——调用它的是你,你知道它干什么。
但 MCP 工具不一样:调用它的是大模型,模型判断的唯一依据就是你写的名字和描述。 描述写得含糊,模型就会在该调的时候不调、不该调的时候乱调,而且你从代码里根本看不出问题——代码是对的,是"说明书"没写清楚。
几条实际经验:
写清楚它做什么,而不是它怎么实现。 "机器人向前移动"是好描述;"发送 DOG_STATE_FORWARD 指令"是坏描述——模型不关心你内部的枚举叫什么。
把边界写进描述。 比如"向前移动一步,约 5 厘米"比"向前移动"有用;带参数的更要说明单位和含义。
相近的动作要能区分开。 如果你有 set_brightness 和 set_rgb 两个工具,描述里就该让模型一眼看出什么时候用哪个。描述模糊的相似工具,是误调用的头号来源。
名字用统一的层级习惯。 参照 self.light.set_rgb 这种 self.设备.动作 的形式,工具多了之后清晰得多。
调试时看什么
工具注册完不生效,按这个顺序看:
一、工具报上去了吗。 设备上线时会把工具清单交给后台,先确认你的新工具在清单里。不在清单里,后面都白搭——这通常是注册代码没执行到,或者编译的不是你改的那份。
二、模型调了吗、调的对不对。 如果工具在清单里但模型不调,八成是描述问题,回去改描述而不是改代码。
三、调了但没动作。 那才轮到查你 lambda 里的硬件控制逻辑,以及引脚、接线这些。
这个顺序很重要:多数人一上来就怀疑硬件,其实前两步的问题更常见,而且查起来快得多。
另外记住上游文档的这句提醒:工具名称、参数及返回值以设备端 AddTool 注册为准。设备是这套能力清单的唯一真相来源,别以为文档里写过就一定和你手上这版一致。
一个动作该拆成几个工具
注册工具时的常见纠结:是做一个"控制灯"的万能工具,还是做"开灯""关灯""调色"好几个?
经验是:按用户会怎么说来拆,不是按代码怎么写方便来拆。
如果用户可能说"把灯关了""开灯""调成红色",那就是三种意图,做成三个语义清晰的工具,模型选起来最准。反过来做一个 light_control(action, params) 的万能工具,模型得先想清楚 action 填什么、params 怎么组,出错概率明显更高。
但也别拆得太碎。把"设置红色""设置绿色""设置蓝色"做成三个工具就过头了——它们本来就该是一个带参数的 set_rgb。
判断标准:如果两个动作的区别可以用一个参数表达,就合成一个带参数的工具;如果区别在意图本身,就拆开。
还有一点:工具数量会影响模型的选择准确率。 几十上百个工具堆在一起,模型选错的概率上升。真做到那个规模时,就该考虑按场景分组,而不是一股脑全注册上去。
为什么新项目该统一用 MCP
上游的建议很明确:推荐所有新项目统一采用 MCP 协议进行物联网控制。
理由不难理解。自己发明一套控制协议,短期看更简单——定几个指令字,收到就执行。但你很快会撞上几件事:新增设备要改后台、模型不知道有哪些能力、参数没有类型和范围约束、换个后台全部重写。
MCP 把这些一次性解决了:能力由设备自己声明,模型自己发现和调用,参数有类型有范围,协议是标准的。 你多花的那点注册代码,换来的是加一个新动作只需要加一个 AddTool——后台一行不用改。
相关:MCP 让它动讲 MCP 在小智里的整体位置,小智接线和默认不一样讲硬件动作那一层的引脚配置,小智自建后端怎么选讲后台侧的方案。