当前位置: 首页 > news >正文

小智ai MCP学习笔记

理论

1、什么是MCP

MCP(Model Context Protocol,模型上下文协议)是一种开放式协议,它实现了LLM与各种工具的调用。使LLM从对话、生成式AI变成了拥有调用三方工具的AI。用官方的比喻,MCP就是USB-C接口,只要实现了这个接口,就可以接入AI,对AI进行赋能。
MCP
其本质是统一了AI调用三方功能的接口,借助AI Agent,使得LLM可以使用三方提供的服务来处理用户提出的问题。

从上图可以看到一些MCP的相关概念
MCP server:提供服务的三方,需要实现MCP server的功能,即将提供的功能接口按照MCP协议规定的格式,告知MCP client。
MCP client:连接MCP server与LLM的桥梁,负责管理与MCP server一对一的连接。
MCP hosts:一般指AI应用,通常由AI Agent实现MCP client的功能,再由AI Agent作为MCP hosts。
除此之外,还需要知道MCP tools的概念,第三方提供的功能接口一般称为一个tool,在后面的代码中会展示这一点。

这里引用up隔壁的程序员老王的一张视频截图,很清晰的展示了从用户提问,到AI返回结果这一过程中,是如何调用三方MCP服务的。原视频:10分钟讲清楚 Prompt, Agent, MCP 是什么
在这里插入图片描述

2、小智AI MCP server

下面回到小智AI中,在虾哥提供的源码中,实现了MCP server,目前该功能还在内测中(2025年7月20日),可以去小智官网看看使用教程。

关于MCP协议的格式这里也不再复述,菜鸟教程和xiaozhi-esp32源码的/docs/mcp-protocol.md中有非常详细的介绍。这里只关注MCP的核心逻辑。

我们来看看MCP server的源码。最关键的类就是McpServer,这个类实现了注册工具、解析响应、调用工具等功能。

class McpServer {
public:static McpServer& GetInstance() {static McpServer instance;return instance;}void AddCommonTools();void AddTool(McpTool* tool);void AddTool(const std::string& name, const std::string& description, const PropertyList& properties, std::function<ReturnValue(const PropertyList&)> callback);void ParseMessage(const cJSON* json);void ParseMessage(const std::string& message);private:McpServer();~McpServer();void ParseCapabilities(const cJSON* capabilities);void ReplyResult(int id, const std::string& result);void ReplyError(int id, const std::string& message);void GetToolsList(int id, const std::string& cursor);void DoToolCall(int id, const std::string& tool_name, const cJSON* tool_arguments, int stack_size);std::vector<McpTool*> tools_;std::thread tool_call_thread_;
};

AddCommonTools()
这个方法实现了注册工具的功能,在Application::Start()中调用。

    // Add MCP common tools before initializing the protocol
#if CONFIG_IOT_PROTOCOL_MCPMcpServer::GetInstance().AddCommonTools();
#endif

其具体实现没什么神秘的,就是调用AddTool将功能接口的信息、参数和接口push进tools队列。
比如设置音量的接口:

    AddTool("self.audio_speaker.set_volume", "Set the volume of the audio speaker. If the current volume is unknown, you must call `self.get_device_status` tool first and then call this tool.",PropertyList({Property("volume", kPropertyTypeInteger, 0, 100)}), [&board](const PropertyList& properties) -> ReturnValue {auto codec = board.GetAudioCodec();codec->SetOutputVolume(properties["volume"].value<int>());return true;});
void McpServer::AddTool(McpTool* tool) {// Prevent adding duplicate toolsif (std::find_if(tools_.begin(), tools_.end(), [tool](const McpTool* t) { return t->name() == tool->name(); }) != tools_.end()) {ESP_LOGW(TAG, "Tool %s already added", tool->name().c_str());return;}ESP_LOGI(TAG, "Add tool: %s", tool->name().c_str());tools_.push_back(tool);
}void McpServer::AddTool(const std::string& name, const std::string& description, const PropertyList& properties, std::function<ReturnValue(const PropertyList&)> callback) {AddTool(new McpTool(name, description, properties, callback));
}

ParseMessage()
解析收到的JSON,对JSON格式校验,如果是调用tool就调用DoToolCall去执行对应的tool。

void McpServer::ParseMessage(const cJSON* json) {// Check JSONRPC versionauto version = cJSON_GetObjectItem(json, "jsonrpc");if (version == nullptr || !cJSON_IsString(version) || strcmp(version->valuestring, "2.0") != 0) {ESP_LOGE(TAG, "Invalid JSONRPC version: %s", version ? version->valuestring : "null");return;}// Check methodauto method = cJSON_GetObjectItem(json, "method");if (method == nullptr || !cJSON_IsString(method)) {ESP_LOGE(TAG, "Missing method");return;}...if (method_str == "tools/call") {...DoToolCall(id_int, std::string(tool_name->valuestring), tool_arguments, stack_size ? stack_size->valueint : DEFAULT_TOOLCALL_STACK_SIZE);} else {ESP_LOGE(TAG, "Method not implemented: %s", method_str.c_str());ReplyError(id_int, "Method not implemented: " + method_str);}
}

DoToolCall()
查找tool,创建新的线程,在新线程中调用tool中的回调函数,即三方实现的功能接口。

void McpServer::DoToolCall(int id, const std::string& tool_name, const cJSON* tool_arguments, int stack_size) {// 在tools中按tool_name查找toolauto tool_iter = std::find_if(tools_.begin(), tools_.end(), [&tool_name](const McpTool* tool) { return tool->name() == tool_name; });// 解析回调函数的参数PropertyList arguments = (*tool_iter)->properties();try {for (auto& argument : arguments) {...}}// Start a task to receive data with stack sizeesp_pthread_cfg_t cfg = esp_pthread_get_default_config();cfg.thread_name = "tool_call";cfg.stack_size = stack_size;cfg.prio = 1;esp_pthread_set_cfg(&cfg);// Use a thread to call the tool to avoid blocking the main threadtool_call_thread_ = std::thread([this, id, tool_iter, arguments = std::move(arguments)]() {try {ReplyResult(id, (*tool_iter)->Call(arguments));} catch (const std::exception& e) {ESP_LOGE(TAG, "tools/call: %s", e.what());ReplyError(id, e.what());}});tool_call_thread_.detach();
}

ReplyResult()ReplyError() 就是将结果转为JSON,并通过protocol(mqtt或websocket)发送出去。

void McpServer::ReplyResult(int id, const std::string& result) {std::string payload = "{\"jsonrpc\":\"2.0\",\"id\":";payload += std::to_string(id) + ",\"result\":";payload += result;payload += "}";Application::GetInstance().SendMcpMessage(payload);
}void Application::SendMcpMessage(const std::string& payload) {Schedule([this, payload]() {if (protocol_) {protocol_->SendMcpMessage(payload);}});
}

通过对源码的分析,我们知道了MCP server的核心逻辑,简单来说就是在server中将接口放入tools,之后由MCP client发起调用,server解析JSON后去调用对应的接口。至于client是如何知道有哪些tool的,可以在ParseMessage()中发现端倪:

if (method_str == "tools/list") {std::string cursor_str = "";if (params != nullptr) {auto cursor = cJSON_GetObjectItem(params, "cursor");if (cJSON_IsString(cursor)) {cursor_str = std::string(cursor->valuestring);}}GetToolsList(id_int, cursor_str);

具体的client代码实现还没有看,但不难猜测,client会发起一次获取tools的请求,这样就知道了有哪些tool。

下面我们就可以注册一个自己的tool,来实现对外设的控制。

实践

实现功能:语音控制灯光亮度
首先初始化RGB灯

// 配置定时器
ledc_timer_config_t ledc_timer = {.speed_mode = LEDC_LOW_SPEED_MODE,.duty_resolution = LEDC_TIMER_8_BIT,.timer_num = LEDC_TIMER_0,.freq_hz = 5000,
};
ESP_ERROR_CHECK(ledc_timer_config(&ledc_timer));// 配置通道
ledc_channel_config_t ledc_channel={.gpio_num = GPIO_NUM_1,.speed_mode = LEDC_LOW_SPEED_MODE,.channel = LEDC_CHANNEL_0,.timer_sel = LEDC_TIMER_0,.duty = 0,
};
ESP_ERROR_CHECK(ledc_channel_config(&ledc_channel));

然后添加tool

AddTool("self.my_led.set_brightness","Set the brightness of the blue LED. The brightness is a percentage value from 0 to 100.",PropertyList({Property("brightness", kPropertyTypeInteger, 0, 100)}),[](const PropertyList& properties) -> ReturnValue {uint32_t brightness = static_cast<uint32_t>(properties["brightness"].value<int>());ESP_LOGI(TAG, "my led set brightness %lu", brightness);if (brightness > 100) {brightness = 100;}uint32_t duty = (brightness * 255) / 100; // Convert to 8-bit duty cycleESP_ERROR_CHECK(ledc_set_duty(LEDC_LOW_SPEED_MODE, LEDC_CHANNEL_0, duty));ESP_ERROR_CHECK(ledc_update_duty(LEDC_LOW_SPEED_MODE, LEDC_CHANNEL_0));return true;});

效果如下:

小智AI使用MCP控制RGB灯光亮度

http://www.dtcms.com/a/290793.html

相关文章:

  • 2021 RoboCom 世界机器人开发者大赛-本科组(复赛)解题报告 | 珂学家
  • SOLIDWORK教育版实时协作打破空间限制
  • 组合设计模式
  • 笔试——Day14
  • tcp的三次握手与四次挥手
  • 手机录制视频时,硬编码和软编码哪个质量高?(硬件编码、软件编码)
  • 跨端分栏布局:从手机到Pad的优雅切换
  • 删除 XML 格式中双引号内的空格
  • odoo-059 xml中字段上写 domain 和 filter_domain 什么区别
  • 对理性决策模型的剖析及应用路径
  • vue2使用v-viewer图片预览:打开页面自动预览,禁止关闭预览,解决在微信浏览器的页面点击事件老是触发预览初始化的问题
  • OpenEuler 22.03 系统上安装配置gitlab runner
  • Linux网络编程-osi、udp
  • nextjs+react接口会请求两次?
  • react class和function 如何模拟vue中的 双向绑定 监听 computed的方式
  • macbookpro m1 max本儿上速搭一个elasticsearch+kibana环境
  • Electron 主进程与渲染进程之间交互方式
  • Spring Boot自动装配原理深度解析:从核心注解到实现机制
  • Electron使用WebAssembly实现CRC-16 原理校验
  • YModem在Android上的实现(四)
  • 自动化测试概念和时机
  • Java -- 自定义异常--Wrapper类--String类
  • MySQL表的基础操作
  • 第三章自定义检视面板_创建自定义编辑器类_编扩展默认组件的显示面板(本章进度6/9)
  • 奥比中光深度相机开发
  • java学习day6 + leetcode31 下一个排列
  • 面向对象编程 - 高级篇一
  • 力扣面试150题--搜索二维矩阵
  • TPAMI 2025 | 空间频率调制(SFM),深度学习语义分割的细节守护者
  • uniapp 输入时动态修改值(如含单位)光标被强制移至末尾