理論
1、什么是MCP
MCP(Model Context Protocol,模型上下文協議)是一種開放式協議,它實現了LLM與各種工具的調用。使LLM從對話、生成式AI變成了擁有調用三方工具的AI。用官方的比喻,MCP就是USB-C接口,只要實現了這個接口,就可以接入AI,對AI進行賦能。
其本質是統一了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燈光亮度