123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367 |
- /*
- * MCP Server Implementation
- * Reference: https://modelcontextprotocol.io/specification/2024-11-05
- */
- #include "mcp_server.h"
- #include <esp_log.h>
- #include <esp_app_desc.h>
- #include <algorithm>
- #include <cstring>
- #include <esp_pthread.h>
- #include "application.h"
- #include "display.h"
- #include "board.h"
- #define TAG "MCP"
- #define DEFAULT_TOOLCALL_STACK_SIZE 6144
- McpServer::McpServer() {
- }
- McpServer::~McpServer() {
- for (auto tool : tools_) {
- delete tool;
- }
- tools_.clear();
- }
- void McpServer::AddCommonTools() {
- // To speed up the response time, we add the common tools to the beginning of
- // the tools list to utilize the prompt cache.
- // Backup the original tools list and restore it after adding the common tools.
- auto original_tools = std::move(tools_);
- auto& board = Board::GetInstance();
- AddTool("self.get_device_status",
- "Provides the real-time information of the device, including the current status of the audio speaker, screen, battery, network, etc.\n"
- "Use this tool for: \n"
- "1. Answering questions about current condition (e.g. what is the current volume of the audio speaker?)\n"
- "2. As the first step to control the device (e.g. turn up / down the volume of the audio speaker, etc.)",
- PropertyList(),
- [&board](const PropertyList& properties) -> ReturnValue {
- return board.GetDeviceStatusJson();
- });
- 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;
- });
-
- auto backlight = board.GetBacklight();
- if (backlight) {
- AddTool("self.screen.set_brightness",
- "Set the brightness of the screen.",
- PropertyList({
- Property("brightness", kPropertyTypeInteger, 0, 100)
- }),
- [backlight](const PropertyList& properties) -> ReturnValue {
- uint8_t brightness = static_cast<uint8_t>(properties["brightness"].value<int>());
- backlight->SetBrightness(brightness, true);
- return true;
- });
- }
- auto display = board.GetDisplay();
- if (display && !display->GetTheme().empty()) {
- AddTool("self.screen.set_theme",
- "Set the theme of the screen. The theme can be `light` or `dark`.",
- PropertyList({
- Property("theme", kPropertyTypeString)
- }),
- [display](const PropertyList& properties) -> ReturnValue {
- display->SetTheme(properties["theme"].value<std::string>().c_str());
- return true;
- });
- }
- auto camera = board.GetCamera();
- if (camera) {
- AddTool("self.camera.take_photo",
- "Take a photo and explain it. Use this tool after the user asks you to see something.\n"
- "Args:\n"
- " `question`: The question that you want to ask about the photo.\n"
- "Return:\n"
- " A JSON object that provides the photo information.",
- PropertyList({
- Property("question", kPropertyTypeString)
- }),
- [camera](const PropertyList& properties) -> ReturnValue {
- if (!camera->Capture()) {
- return "{\"success\": false, \"message\": \"Failed to capture photo\"}";
- }
- auto question = properties["question"].value<std::string>();
- return camera->Explain(question);
- });
- }
- // Restore the original tools list to the end of the tools list
- tools_.insert(tools_.end(), original_tools.begin(), original_tools.end());
- }
- void McpServer::AddTool(McpTool* tool) {
- // Prevent adding duplicate tools
- if (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));
- }
- void McpServer::ParseMessage(const std::string& message) {
- cJSON* json = cJSON_Parse(message.c_str());
- if (json == nullptr) {
- ESP_LOGE(TAG, "Failed to parse MCP message: %s", message.c_str());
- return;
- }
- ParseMessage(json);
- cJSON_Delete(json);
- }
- void McpServer::ParseCapabilities(const cJSON* capabilities) {
- auto vision = cJSON_GetObjectItem(capabilities, "vision");
- if (cJSON_IsObject(vision)) {
- auto url = cJSON_GetObjectItem(vision, "url");
- auto token = cJSON_GetObjectItem(vision, "token");
- if (cJSON_IsString(url)) {
- auto camera = Board::GetInstance().GetCamera();
- if (camera) {
- std::string url_str = std::string(url->valuestring);
- std::string token_str;
- if (cJSON_IsString(token)) {
- token_str = std::string(token->valuestring);
- }
- camera->SetExplainUrl(url_str, token_str);
- }
- }
- }
- }
- void McpServer::ParseMessage(const cJSON* json) {
- // Check JSONRPC version
- auto 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 method
- auto method = cJSON_GetObjectItem(json, "method");
- if (method == nullptr || !cJSON_IsString(method)) {
- ESP_LOGE(TAG, "Missing method");
- return;
- }
-
- auto method_str = std::string(method->valuestring);
- if (method_str.find("notifications") == 0) {
- return;
- }
-
- // Check params
- auto params = cJSON_GetObjectItem(json, "params");
- if (params != nullptr && !cJSON_IsObject(params)) {
- ESP_LOGE(TAG, "Invalid params for method: %s", method_str.c_str());
- return;
- }
- auto id = cJSON_GetObjectItem(json, "id");
- if (id == nullptr || !cJSON_IsNumber(id)) {
- ESP_LOGE(TAG, "Invalid id for method: %s", method_str.c_str());
- return;
- }
- auto id_int = id->valueint;
-
- if (method_str == "initialize") {
- if (cJSON_IsObject(params)) {
- auto capabilities = cJSON_GetObjectItem(params, "capabilities");
- if (cJSON_IsObject(capabilities)) {
- ParseCapabilities(capabilities);
- }
- }
- auto app_desc = esp_app_get_description();
- std::string message = "{\"protocolVersion\":\"2024-11-05\",\"capabilities\":{\"tools\":{}},\"serverInfo\":{\"name\":\"" BOARD_NAME "\",\"version\":\"";
- message += app_desc->version;
- message += "\"}}";
- ReplyResult(id_int, message);
- } else 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);
- } else if (method_str == "tools/call") {
- if (!cJSON_IsObject(params)) {
- ESP_LOGE(TAG, "tools/call: Missing params");
- ReplyError(id_int, "Missing params");
- return;
- }
- auto tool_name = cJSON_GetObjectItem(params, "name");
- if (!cJSON_IsString(tool_name)) {
- ESP_LOGE(TAG, "tools/call: Missing name");
- ReplyError(id_int, "Missing name");
- return;
- }
- auto tool_arguments = cJSON_GetObjectItem(params, "arguments");
- if (tool_arguments != nullptr && !cJSON_IsObject(tool_arguments)) {
- ESP_LOGE(TAG, "tools/call: Invalid arguments");
- ReplyError(id_int, "Invalid arguments");
- return;
- }
- auto stack_size = cJSON_GetObjectItem(params, "stackSize");
- if (stack_size != nullptr && !cJSON_IsNumber(stack_size)) {
- ESP_LOGE(TAG, "tools/call: Invalid stackSize");
- ReplyError(id_int, "Invalid stackSize");
- return;
- }
- 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);
- }
- }
- 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 McpServer::ReplyError(int id, const std::string& message) {
- std::string payload = "{\"jsonrpc\":\"2.0\",\"id\":";
- payload += std::to_string(id);
- payload += ",\"error\":{\"message\":\"";
- payload += message;
- payload += "\"}}";
- Application::GetInstance().SendMcpMessage(payload);
- }
- void McpServer::GetToolsList(int id, const std::string& cursor) {
- const int max_payload_size = 8000;
- std::string json = "{\"tools\":[";
-
- bool found_cursor = cursor.empty();
- auto it = tools_.begin();
- std::string next_cursor = "";
-
- while (it != tools_.end()) {
- // 如果我们还没有找到起始位置,继续搜索
- if (!found_cursor) {
- if ((*it)->name() == cursor) {
- found_cursor = true;
- } else {
- ++it;
- continue;
- }
- }
-
- // 添加tool前检查大小
- std::string tool_json = (*it)->to_json() + ",";
- if (json.length() + tool_json.length() + 30 > max_payload_size) {
- // 如果添加这个tool会超出大小限制,设置next_cursor并退出循环
- next_cursor = (*it)->name();
- break;
- }
-
- json += tool_json;
- ++it;
- }
-
- if (json.back() == ',') {
- json.pop_back();
- }
-
- if (json.back() == '[' && !tools_.empty()) {
- // 如果没有添加任何tool,返回错误
- ESP_LOGE(TAG, "tools/list: Failed to add tool %s because of payload size limit", next_cursor.c_str());
- ReplyError(id, "Failed to add tool " + next_cursor + " because of payload size limit");
- return;
- }
- if (next_cursor.empty()) {
- json += "]}";
- } else {
- json += "],\"nextCursor\":\"" + next_cursor + "\"}";
- }
-
- ReplyResult(id, json);
- }
- void McpServer::DoToolCall(int id, const std::string& tool_name, const cJSON* tool_arguments, int stack_size) {
- auto tool_iter = std::find_if(tools_.begin(), tools_.end(),
- [&tool_name](const McpTool* tool) {
- return tool->name() == tool_name;
- });
-
- if (tool_iter == tools_.end()) {
- ESP_LOGE(TAG, "tools/call: Unknown tool: %s", tool_name.c_str());
- ReplyError(id, "Unknown tool: " + tool_name);
- return;
- }
- PropertyList arguments = (*tool_iter)->properties();
- try {
- for (auto& argument : arguments) {
- bool found = false;
- if (cJSON_IsObject(tool_arguments)) {
- auto value = cJSON_GetObjectItem(tool_arguments, argument.name().c_str());
- if (argument.type() == kPropertyTypeBoolean && cJSON_IsBool(value)) {
- argument.set_value<bool>(value->valueint == 1);
- found = true;
- } else if (argument.type() == kPropertyTypeInteger && cJSON_IsNumber(value)) {
- argument.set_value<int>(value->valueint);
- found = true;
- } else if (argument.type() == kPropertyTypeString && cJSON_IsString(value)) {
- argument.set_value<std::string>(value->valuestring);
- found = true;
- }
- }
- if (!argument.has_default_value() && !found) {
- ESP_LOGE(TAG, "tools/call: Missing valid argument: %s", argument.name().c_str());
- ReplyError(id, "Missing valid argument: " + argument.name());
- return;
- }
- }
- } catch (const std::exception& e) {
- ESP_LOGE(TAG, "tools/call: %s", e.what());
- ReplyError(id, e.what());
- return;
- }
- // Start a task to receive data with stack size
- esp_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 thread
- tool_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();
- }
|