侧边栏壁纸
  • 累计撰写 20 篇文章
  • 累计创建 10 个标签
  • 累计收到 3 条评论

目 录CONTENT

文章目录

xiaozhi-esp32: MCP流程详解

王富贵
2026-04-22 / 0 评论 / 0 点赞 / 9 阅读 / 0 字

xiaozhi-esp32: MCP流程详解

本文档详细描述了 xiaozhi-esp32 项目中 MCP(Model Context Protocol)的完整运行流程,包括 WebSocket 握手后 LLM 如何发现 MCP 工具,以及如何调用 MCP 工具。

概述

MCP 是本项目中用于后台 API(MCP 客户端)与 ESP32 设备(MCP 服务器)之间通信的协议。它基于 JSON-RPC 2.0 规范,承载在 WebSocket 或 MQTT 等基础通信协议之上。

核心角色:

  • ESP32 设备:作为 MCP Server,注册并暴露工具(Tool),响应后台的工具发现和调用请求
  • 后台 API:作为 MCP Client,负责发现设备工具、将工具信息提供给 LLM、并代理 LLM 的工具调用请求
  • LLM(大语言模型):通过后台 API 间接感知设备工具,并在对话中决定是否调用

一、连接建立与能力通告

1.1 WebSocket 连接

设备启动后,通过 WebsocketProtocol::OpenAudioChannel() 建立 WebSocket 连接。连接时会在 HTTP Header 中携带认证信息:

Authorization: Bearer <token>
Protocol-Version: <version>
Device-Id: <mac_address>
Client-Id: <uuid>

关键代码路径main/protocols/websocket_protocol.ccOpenAudioChannel()

1.2 Hello 握手

WebSocket 连接成功后,设备立即发送 Hello 消息,其中通过 features.mcp: true 告知后台"本设备支持 MCP 协议":

{
  "type": "hello",
  "version": 1,
  "features": {
    "mcp": true
  },
  "transport": "websocket",
  "audio_params": {
    "format": "opus",
    "sample_rate": 16000,
    "channels": 1,
    "frame_duration": 20
  }
}

关键代码路径main/protocols/websocket_protocol.ccGetHelloMessage()

1.3 服务端 Hello 响应

后台收到设备 Hello 后,返回服务端 Hello,包含 session_id 和音频参数等信息。设备解析后保存 session_id,后续所有消息都会携带此 ID。

关键代码路径main/protocols/websocket_protocol.ccParseServerHello()


二、MCP 会话初始化

后台确认设备支持 MCP 后,开始 MCP 会话的初始化流程。

2.1 Initialize 请求

后台发送 initialize 方法,可携带客户端能力(如摄像头视觉处理地址):

{
  "session_id": "...",
  "type": "mcp",
  "payload": {
    "jsonrpc": "2.0",
    "method": "initialize",
    "params": {
      "capabilities": {
        "vision": {
          "url": "http://...",
          "token": "..."
        }
      }
    },
    "id": 1
  }
}

2.2 设备处理 Initialize

设备收到后,McpServer::ParseMessage() 解析 JSON-RPC 消息:

  1. 验证 jsonrpc 版本为 "2.0"
  2. 识别 method"initialize"
  3. 解析 capabilities(如设置摄像头的图片处理 URL)
  4. 返回设备信息和协议版本
{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "protocolVersion": "2024-11-05",
    "capabilities": {
      "tools": {}
    },
    "serverInfo": {
      "name": "xiaozhi-board-name",
      "version": "1.0.0"
    }
  }
}

关键代码路径main/mcp_server.ccParseMessage()method_str == "initialize" 分支


三、LLM 如何知道有哪些 MCP 工具

这是整个流程的核心环节。LLM 本身不直接与设备通信,而是通过后台 API 间接获取工具信息。

3.1 工具注册(设备启动时)

设备在 Application::Initialize() 中调用 McpServer::AddCommonTools()McpServer::AddUserOnlyTools() 注册所有工具:

// main/application.cc → Initialize()
auto& mcp_server = McpServer::GetInstance();
mcp_server.AddCommonTools();    // 注册通用工具(对 AI 可见)
mcp_server.AddUserOnlyTools();  // 注册仅用户可见的工具(对 AI 不可见)

通用工具(AI 可见) 包括:

工具名功能
self.get_device_status获取设备实时状态(音量、电池、网络等)
self.audio_speaker.set_volume设置音量
self.screen.set_brightness设置屏幕亮度
self.screen.set_theme设置主题(light/dark)
self.camera.take_photo拍照并解释

仅用户可见工具(AI 不可见) 包括:

工具名功能
self.get_system_info获取系统信息
self.reboot重启设备
self.upgrade_firmware固件升级
self.screen.get_info获取屏幕信息
self.screen.snapshot屏幕截图

user_only 机制:通过 annotations.audience: ["user"] 标记,后台在将工具列表传递给 LLM 时会过滤掉这些工具,确保 AI 不会主动调用敏感操作。

3.2 后台获取工具列表(tools/list)

后台通过 tools/list 方法向设备请求工具列表:

{
  "session_id": "...",
  "type": "mcp",
  "payload": {
    "jsonrpc": "2.0",
    "method": "tools/list",
    "params": {
      "cursor": ""
    },
    "id": 2
  }
}

设备返回工具列表,每个工具包含 namedescriptioninputSchema

{
  "jsonrpc": "2.0",
  "id": 2,
  "result": {
    "tools": [
      {
        "name": "self.get_device_status",
        "description": "Provides the real-time information of the device...",
        "inputSchema": {
          "type": "object",
          "properties": {}
        }
      },
      {
        "name": "self.audio_speaker.set_volume",
        "description": "Set the volume of the audio speaker...",
        "inputSchema": {
          "type": "object",
          "properties": {
            "volume": {
              "type": "integer",
              "minimum": 0,
              "maximum": 100
            }
          },
          "required": ["volume"]
        }
      }
    ],
    "nextCursor": ""
  }
}

关键代码路径main/mcp_server.ccGetToolsList()

3.3 分页机制

由于 ESP32 内存有限,工具列表的 payload 大小限制为 8000 字节。如果工具过多,会通过 nextCursor 实现分页:

const int max_payload_size = 8000;
// 如果添加这个 tool 会超出大小限制,设置 next_cursor 并退出循环
if (json.length() + tool_json.length() + 30 > max_payload_size) {
    next_cursor = (*it)->name();
    break;
}

后台收到非空的 nextCursor 后,需要再次请求 tools/list 并带上 cursor 参数。

3.4 后台将工具信息注入 LLM

后台拿到设备的工具列表后,将其转换为 LLM 的 function calling / tool use 格式,注入到 LLM 的 system prompt 或 tools 参数中。这样 LLM 在对话时就"知道"了设备有哪些可用工具。

这一步发生在后台服务端,不在 ESP32 设备代码中。


四、MCP 工具调用流程

当用户通过语音对话触发 LLM 决定调用某个工具时,完整的调用链路如下:

4.1 完整调用链路

用户语音 → 设备采集音频 → WebSocket 发送到后台
    → 后台 ASR 转文字 → 发送给 LLM
    → LLM 决定调用工具(function call)
    → 后台构造 MCP tools/call 请求 → WebSocket 发送到设备
    → 设备 McpServer 执行工具 → 返回结果
    → 后台将结果返回给 LLM → LLM 生成回复
    → 后台 TTS 合成语音 → WebSocket 发送到设备播放

4.2 消息路由

设备端收到 WebSocket 消息后的路由流程:

WebSocket 收到 JSON 消息
    → websocket_protocol.cc: OnData 回调
    → 判断 type 字段
        → "hello" → ParseServerHello()
        → 其他   → on_incoming_json_ 回调
            → application.cc: OnIncomingJson 处理
                → type == "tts"  → 语音播放控制
                → type == "stt"  → 语音识别结果显示
                → type == "llm"  → 表情控制
                → type == "mcp"  → McpServer::ParseMessage(payload)

关键代码application.cc 中的 MCP 消息路由):

protocol_->OnIncomingJson([this, display](const cJSON* root) {
    auto type = cJSON_GetObjectItem(root, "type");
    // ...
    if (strcmp(type->valuestring, "mcp") == 0) {
        auto payload = cJSON_GetObjectItem(root, "payload");
        if (cJSON_IsObject(payload)) {
            McpServer::GetInstance().ParseMessage(payload);
        }
    }
});

4.3 tools/call 请求处理

后台发送工具调用请求:

{
  "session_id": "...",
  "type": "mcp",
  "payload": {
    "jsonrpc": "2.0",
    "method": "tools/call",
    "params": {
      "name": "self.audio_speaker.set_volume",
      "arguments": {
        "volume": 50
      }
    },
    "id": 3
  }
}

设备端 McpServer::ParseMessage() 处理流程:

  1. 验证 JSON-RPC 格式:检查 jsonrpcmethodid 字段
  2. 查找工具:在 tools_ 列表中按 name 查找对应的 McpTool
  3. 解析参数:将 arguments 中的值填充到工具的 PropertyList 中,并进行类型和范围校验
  4. 调度执行:通过 Application::Schedule() 将工具调用调度到主线程执行,确保线程安全
  5. 返回结果:执行完成后通过 ReplyResult() 返回结果

关键代码路径main/mcp_server.ccDoToolCall()

void McpServer::DoToolCall(int id, const std::string& tool_name, const cJSON* tool_arguments) {
    // 1. 查找工具
    auto tool_iter = std::find_if(tools_.begin(), tools_.end(), ...);
    
    // 2. 解析参数并校验
    PropertyList arguments = (*tool_iter)->properties();
    for (auto& argument : arguments) {
        // 类型匹配、范围校验...
    }
    
    // 3. 调度到主线程执行
    app.Schedule([this, id, tool_iter, arguments]() {
        try {
            ReplyResult(id, (*tool_iter)->Call(arguments));
        } catch (const std::exception& e) {
            ReplyError(id, e.what());
        }
    });
}

4.4 工具执行结果

成功响应

{
  "session_id": "...",
  "type": "mcp",
  "payload": {
    "jsonrpc": "2.0",
    "id": 3,
    "result": {
      "content": [
        { "type": "text", "text": "true" }
      ],
      "isError": false
    }
  }
}

错误响应

{
  "session_id": "...",
  "type": "mcp",
  "payload": {
    "jsonrpc": "2.0",
    "id": 3,
    "error": {
      "message": "Unknown tool: self.non_existent_tool"
    }
  }
}

4.5 结果返回类型

工具的返回值支持多种类型(ReturnValue = std::variant<bool, int, std::string, cJSON*, ImageContent*>):

返回类型序列化方式
bool"true""false"
int数字字符串,如 "42"
std::string原始字符串
cJSON*JSON 格式字符串
ImageContent*Base64 编码的图片数据

五、完整交互时序图

sequenceDiagram
    participant User as 用户
    participant Device as ESP32 设备
    participant Backend as 后台 API
    participant LLM as 大语言模型

    Note over Device, Backend: 阶段一:连接建立

    Device->>Backend: WebSocket 连接(携带 Auth Header)
    Device->>Backend: Hello 消息(features.mcp: true)
    Backend->>Device: Server Hello(session_id)

    Note over Device, Backend: 阶段二:MCP 会话初始化

    Backend->>Device: MCP initialize(capabilities)
    Device->>Backend: initialize 响应(serverInfo, protocolVersion)

    Backend->>Device: MCP tools/list(cursor: "")
    Device->>Backend: tools/list 响应(工具列表 + nextCursor)

    opt 分页获取
        Backend->>Device: MCP tools/list(cursor: "...")
        Device->>Backend: tools/list 响应(更多工具)
    end

    Backend->>LLM: 注入工具定义到 LLM(function calling 格式)

    Note over User, LLM: 阶段三:对话与工具调用

    User->>Device: 语音输入:"帮我把音量调到50"
    Device->>Backend: 音频流(Opus 编码)
    Backend->>Backend: ASR 语音识别
    Backend->>LLM: 用户文本 + 可用工具列表

    LLM->>Backend: 决定调用 self.audio_speaker.set_volume(volume=50)

    Backend->>Device: MCP tools/call(name, arguments)
    Device->>Device: McpServer 执行工具(主线程)
    Device->>Backend: 工具执行结果

    Backend->>LLM: 工具执行结果
    LLM->>Backend: 生成回复:"已将音量调整到50"

    Backend->>Device: TTS 音频流
    Device->>User: 播放语音回复

六、消息封装格式

所有 MCP 消息都封装在基础协议的消息体中,外层包含 session_idtype,内层 payload 是标准的 JSON-RPC 2.0 消息:

{
  "session_id": "abc123",
  "type": "mcp",
  "payload": {
    "jsonrpc": "2.0",
    "method": "tools/call",
    "params": { ... },
    "id": 1
  }
}

发送路径(设备 → 后台):

McpServer::ReplyResult() / ReplyError()
    → Application::SendMcpMessage(payload)
        → Protocol::SendMcpMessage(payload)
            → 封装为 {"session_id":"...", "type":"mcp", "payload": ...}
            → WebSocket / MQTT SendText()

接收路径(后台 → 设备):

WebSocket OnData 回调
    → Protocol::on_incoming_json_
        → Application: 判断 type == "mcp"
            → McpServer::ParseMessage(payload)
                → 根据 method 分发:initialize / tools/list / tools/call

七、关键设计要点

7.1 工具优先级与 Prompt Cache

通用工具(AddCommonTools)被放在工具列表的最前面,利用 LLM 的 prompt cache 特性加速响应:

void McpServer::AddCommonTools() {
    // 备份原有工具列表,先添加通用工具,再恢复原有工具
    auto original_tools = std::move(tools_);
    // ... 添加通用工具 ...
    tools_.insert(tools_.end(), original_tools.begin(), original_tools.end());
}

7.2 线程安全

工具的实际执行通过 Application::Schedule() 调度到主线程(FreeRTOS 事件循环),避免多线程并发问题:

app.Schedule([this, id, tool_iter, arguments]() {
    try {
        ReplyResult(id, (*tool_iter)->Call(arguments));
    } catch (const std::exception& e) {
        ReplyError(id, e.what());
    }
});

7.3 自定义工具扩展

各硬件板子可以在自己的 InitializeTools() 函数中注册自定义工具,例如机器人控制、灯光控制等:

// 示例:注册一个控制机器人前进的工具
mcp_server.AddTool("self.dog.forward", "机器人向前移动",
    PropertyList(),
    [this](const PropertyList&) -> ReturnValue {
        servo_dog_ctrl_send(DOG_STATE_FORWARD, NULL);
        return true;
    });

7.4 参数类型与校验

工具参数支持三种类型,并可指定范围和默认值:

类型枚举值支持范围支持默认值
布尔kPropertyTypeBoolean
整数kPropertyTypeInteger是(min/max)
字符串kPropertyTypeString

八、相关源码文件

文件说明
main/mcp_server.hMCP 服务器头文件,定义 McpTool、Property、McpServer 等类
main/mcp_server.ccMCP 服务器实现,工具注册、消息解析、工具调用
main/protocols/protocol.h协议基类,定义 SendMcpMessage 等接口
main/protocols/protocol.cc协议基类实现,MCP 消息封装
main/protocols/websocket_protocol.ccWebSocket 协议实现,Hello 握手、消息收发
main/application.cc应用主逻辑,MCP 消息路由、工具初始化
0

评论区