2025-05-22 19:19:36 +08:00
|
|
|
|
/*
|
|
|
|
|
|
* 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 "application.h"
|
|
|
|
|
|
#include "display.h"
|
|
|
|
|
|
#include "board.h"
|
|
|
|
|
|
|
|
|
|
|
|
#define TAG "MCP"
|
|
|
|
|
|
|
|
|
|
|
|
McpServer::McpServer() {
|
|
|
|
|
|
AddCommonTools();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-05-25 17:07:07 +08:00
|
|
|
|
McpServer::~McpServer() {
|
|
|
|
|
|
for (auto tool : tools_) {
|
|
|
|
|
|
delete tool;
|
|
|
|
|
|
}
|
|
|
|
|
|
tools_.clear();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-05-22 19:19:36 +08:00
|
|
|
|
void McpServer::AddCommonTools() {
|
2025-05-25 17:07:07 +08:00
|
|
|
|
auto& board = Board::GetInstance();
|
|
|
|
|
|
|
2025-05-22 19:19:36 +08:00
|
|
|
|
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(),
|
2025-05-25 17:07:07 +08:00
|
|
|
|
[&board](const PropertyList& properties) -> ReturnValue {
|
|
|
|
|
|
return board.GetDeviceStatusJson();
|
2025-05-22 19:19:36 +08:00
|
|
|
|
});
|
|
|
|
|
|
|
2025-05-23 04:26:47 +08:00
|
|
|
|
AddTool("self.audio_speaker.set_volume",
|
2025-05-22 19:19:36 +08:00
|
|
|
|
"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)
|
|
|
|
|
|
}),
|
2025-05-25 17:07:07 +08:00
|
|
|
|
[&board](const PropertyList& properties) -> ReturnValue {
|
|
|
|
|
|
auto codec = board.GetAudioCodec();
|
2025-05-22 19:19:36 +08:00
|
|
|
|
codec->SetOutputVolume(properties["volume"].value<int>());
|
|
|
|
|
|
return true;
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2025-05-25 17:07:07 +08:00
|
|
|
|
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>());
|
2025-05-22 19:19:36 +08:00
|
|
|
|
backlight->SetBrightness(brightness, true);
|
2025-05-25 17:07:07 +08:00
|
|
|
|
return true;
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
2025-05-22 19:19:36 +08:00
|
|
|
|
|
2025-05-25 17:07:07 +08:00
|
|
|
|
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 {
|
2025-05-22 19:19:36 +08:00
|
|
|
|
display->SetTheme(properties["theme"].value<std::string>().c_str());
|
2025-05-25 17:07:07 +08:00
|
|
|
|
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);
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
2025-05-22 19:19:36 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void McpServer::AddTool(McpTool* tool) {
|
2025-05-25 17:07:07 +08:00
|
|
|
|
ESP_LOGI(TAG, "Add tool: %s", tool->name().c_str());
|
2025-05-22 19:19:36 +08:00
|
|
|
|
tools_.push_back(tool);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void McpServer::AddTool(const std::string& name, const std::string& description, const PropertyList& properties, std::function<ReturnValue(const PropertyList&)> callback) {
|
2025-05-25 17:07:07 +08:00
|
|
|
|
AddTool(new McpTool(name, description, properties, callback));
|
2025-05-22 19:19:36 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-05-25 17:07:07 +08:00
|
|
|
|
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) && cJSON_IsString(token)) {
|
|
|
|
|
|
ESP_LOGI(TAG, "Setting explain URL: %s, token: %s", url->valuestring, token->valuestring);
|
|
|
|
|
|
auto camera = Board::GetInstance().GetCamera();
|
|
|
|
|
|
if (camera) {
|
|
|
|
|
|
camera->SetExplainUrl(std::string(url->valuestring), std::string(token->valuestring));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-05-22 19:19:36 +08:00
|
|
|
|
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") {
|
2025-05-25 17:07:07 +08:00
|
|
|
|
if (cJSON_IsObject(params)) {
|
|
|
|
|
|
auto capabilities = cJSON_GetObjectItem(params, "capabilities");
|
|
|
|
|
|
if (cJSON_IsObject(capabilities)) {
|
|
|
|
|
|
ParseCapabilities(capabilities);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-05-22 19:19:36 +08:00
|
|
|
|
auto app_desc = esp_app_get_description();
|
2025-05-24 03:06:01 +08:00
|
|
|
|
std::string message = "{\"protocolVersion\":\"2024-11-05\",\"capabilities\":{\"tools\":{}},\"serverInfo\":{\"name\":\"" BOARD_NAME "\",\"version\":\"";
|
|
|
|
|
|
message += app_desc->version;
|
|
|
|
|
|
message += "\"}}";
|
|
|
|
|
|
ReplyResult(id_int, message);
|
2025-05-22 19:19:36 +08:00
|
|
|
|
} 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;
|
|
|
|
|
|
}
|
|
|
|
|
|
DoToolCall(id_int, std::string(tool_name->valuestring), tool_arguments);
|
|
|
|
|
|
} 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) {
|
2025-05-24 03:06:01 +08:00
|
|
|
|
std::string payload = "{\"jsonrpc\":\"2.0\",\"id\":";
|
|
|
|
|
|
payload += std::to_string(id) + ",\"result\":";
|
|
|
|
|
|
payload += result;
|
|
|
|
|
|
payload += "}";
|
2025-05-22 19:19:36 +08:00
|
|
|
|
Application::GetInstance().SendMcpMessage(payload);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void McpServer::ReplyError(int id, const std::string& message) {
|
|
|
|
|
|
std::string payload = "{\"jsonrpc\":\"2.0\",\"id\":";
|
2025-05-24 03:06:01 +08:00
|
|
|
|
payload += std::to_string(id);
|
|
|
|
|
|
payload += ",\"error\":{\"message\":\"";
|
|
|
|
|
|
payload += message;
|
|
|
|
|
|
payload += "\"}}";
|
2025-05-22 19:19:36 +08:00
|
|
|
|
Application::GetInstance().SendMcpMessage(payload);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void McpServer::GetToolsList(int id, const std::string& cursor) {
|
2025-05-24 03:06:01 +08:00
|
|
|
|
const int max_payload_size = 8000;
|
2025-05-22 19:19:36 +08:00
|
|
|
|
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) {
|
|
|
|
|
|
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();
|
|
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Application::GetInstance().Schedule([this, id, tool_iter, arguments = std::move(arguments)]() {
|
|
|
|
|
|
try {
|
|
|
|
|
|
ReplyResult(id, (*tool_iter)->Call(arguments));
|
|
|
|
|
|
} catch (const std::runtime_error& e) {
|
|
|
|
|
|
ESP_LOGE(TAG, "tools/call: %s", e.what());
|
|
|
|
|
|
ReplyError(id, e.what());
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|