1、一个开源协议,一个开源落地项目

MCP (Model Context Protocol) is an open-source standard for connecting AI applications to external systems.
MCP(模型上下文协议)是用于将AI应用连接到外部系统的开源标准。
作为一个去年(2024年)11月份发布的开源协议,它在短短一年时间里飞速发展成为AI领域极其重要的协议。

xiaozhi-esp32是由开发者“虾哥”于2024年底在GitHub上开源的面向嵌入式 AI应用的软硬件一体化项目,旨在将大语言模型(LLM)的能力通过Model Context Protocol(MCP)安全、高效地延伸至资源受限的 ESP32 系列芯片上,打造可语音交互、可自主控制物理世界的智能终端。截至写稿时,它在github上拥有了22.7k的Star,也是一个妥妥的明星项目。

本文根据作者使用MCP协议和xiaozhi-esp32的工作经历,解读和分析MCP协议在具体项目的落地实践。

2、MCP协议2025年关键发展回顾

2.1 MCP解决了什么痛点?

在大模型(LLM)应用爆发式增长的背景下,“如何让LLM可靠地调用外部工具”成为工程落地的核心瓶颈。2023–2024年间,各框架各自寻找解决方案:LangChain使用自定义Tool Schema,Ollama依赖函数描述字符串。这种碎片化导致工具无法跨平台复用、上下文状态管理混乱以及边缘设备难以解析非结构化指令等问题。

为了解决这一问题,MCP于2024年中由Anthropic、LangChain社区及多位开源贡献者联合发起,旨在提供一个语言无关、轻量、可扩展的标准化协议,用于描述LLM与外部工具之间的交互上下文。进入2025年以后,MCP从概念验证快速走向生态构建,成为AI工程化基础设施的重要一环。

2.2 2025年MCP核心版本演进

2025年是MCP协议迅速发展的关键一年。全年共发布三个重要规范版本,逐步完善协议语义与工程适配能力。

版本 关键特性
2024-11-05 定义基础消息结构:ToolRequest / ToolResponse,支持 JSON Schema 描述工具参数,无认证机制
2025-03-26 授权升级至 OAuth 2.1;工具注解系统,JSON-RPC批处理;引入 Streamable HTTP(单端点),支持双向通信与断线恢复,简化架构
2025-06-18 新增 Elicitation 协议,支持主动询问用户确认;工具注解扩展为 结构化输出;移除 JSON-RPC 批处理,简化协议;增强断点续传机制
2025-11-25 授权与安全增强:OpenID Connect Discovery 1.0(PR #797)支持标准化授权服务器发现、增量范围授权(Incremental Scope Consent);元数据与可视化;交互与协议扩展

2.3 主流框架支持与官方SDK的双向奔赴

2025年,MCP迅速获得主流AI框架和平台的官方或社区级支持,简直就是遍地开花。比如LangChain很早就开始对MCP提供支持,目前可以通过langchain-mcp-adapters库来使用在MCP服务器上定义的工具。入下图所示:
LangChain

同时,MCP协议也提供了多种开发语言的SDK供开发者使用:
MCP SDK

其中MCP Python SDK是使用人数最多的,也印证了python是AI时代最合适的语言这个说法。

2.4 小结

2025年,MCP协议完成了从“理念”到“基础设施”的关键跨越。其核心价值不仅在于统一了工具调用格式,更在于为LLM与物理世界之间架起了一座可编程的桥梁。

3、xiaozhi-esp32项目概览

3.1 项目简介

xiaozhi-esp32 是由开发者“虾哥”于2024年12月在GitHub上开源的AIoT项目,目标是打造一个低成本、可语音交互、能控制物理设备的边缘AI助手终端
到了今天,项目的描述被精炼概括为:一个基于MCP的聊天机器人|An MCP-based chatbot。
作为语音交互入口,小智AI聊天机器人利用Qwen/DeepSeek等大型模型的AI功能,通过MCP协议实现多终端控制。
xiaozhi-esp32

该项目已经具备了非常完善的AI对话硬件应该具备的功能,包含:

  • 基于 ESP-IDF 的嵌入式固件;
  • 配套的云端服务参考实现;
  • 详细的硬件接线与烧录教程;
  • 面向普通用户的开箱即用体验(支持免开发环境烧录)。

这是让我惊喜异常的,因为我在2024年初开始搞AI音响,当时可参考的项目非常稀少。我还记得当时采用HTTP通信,为了调通协议头而费劲了心思。现在,我可以更新到小智方案了。

3.2 项目整体架构

xiaozhi-esp32 采用典型的“云-边协同”架构,将计算密集型任务(如语音识别、大模型推理)交由云端处理,而指令解析与物理控制则在本地 ESP32 设备完成,兼顾响应速度与功能完整性。
端到端流程如下图:
xiaozhi流程图

3.3 核心功能与硬件支持

3.3.1 主要功能特性

  • 语音交互全流程支持
    支持唤醒词检测(默认“你好小智”)、流式语音上传、TTS 语音播报,形成完整对话闭环。

  • MCP驱动的设备控制
    通过MCP协议调用预定义工具,例如控制底盘前进:

      {
      "jsonrpc": "2.0",
      "method": "tools/call",
      "params": {
          "name": "self.chassis.go_forward",
          "arguments": {}
      },
      "id": 2
      }
    
  • 多模态反馈

    • OLED / LCD 屏幕显示表情或状态(如“思考中…”、“执行成功”);
    • LED呼吸灯随对话状态变化颜色;
    • 语音 + 视觉双重反馈提升用户体验。
  • 低功耗与离线能力

    • 支持深度睡眠模式,待机电流 < 10μA;
    • 唤醒词识别完全本地运行,不依赖网络;
    • 断网时可缓存指令,恢复后重试。

3.3.2 硬件平台兼容性

项目适配多种主流 ESP32 开发板,包括:

平台 特点 典型应用场景
ESP32-S3-BOX 内置麦克风、喇叭、屏幕 桌面语音助手
M5Stack CoreS3 彩色触摸屏 + 扬声器 教学演示、智能家居面板
LILYGO T-Circle-S3 圆形AMOLED + 小巧机身 可穿戴AI吊坠
ESP32-S3开发板 市面上成本非常低的方案 超低成本IoT控制器

此外,项目还支持通过 红外发射管 控制传统家电(如空调、电视),极大扩展了物理控制范围。

3.4 通信与协议栈

xiaozhi-esp32 支持两种主要通信模式,适应不同部署场景:

模式 协议 适用场景
WebSocket模式 WebSocket既传文本又可以传音频 高频交互、低延迟要求(如实时对话)
MQTT + UDP混合模式 MQTT用于控制信令,UDP用于音频流 弱网环境、4G移动网络

所有来自LLM的工具调用指令均封装为 MCP 格式的 JSON 对象,通过上述通道下发至设备。设备端不关心LLM如何生成该消息,只负责“按规执行”即可。

3.5 项目选择MCP带来的好处

引入MCP后,项目获得三大优势:

  1. 标准化:工具描述符合社区规范,便于与其他MCP服务互通;
  2. 可扩展:新增设备只需注册新tool,无需修改通信层;
  3. 安全性:通过schema限制参数范围,防止非法指令(如 brightness=9999)。

4、源码解析:MCP是如何在ESP32上运行的?

4.1 相关源文件简介

由于MCP消息是封装在基础通信协议(如 WebSocket 或 MQTT)的消息体中的,今天的源码分析基于WebSocket通信,所以我们只关心如下几个源文件:

  • main/application.cc
    它是项目的核心主控逻辑模块,协调硬件、网络、音频、协议通信与用户交互,管理设备全生命周期状态,并提供MCP能力集成。
  • main/mcp_server.h, main/mcp_server.cc
    提供了MCP服务端相关的工具类,包括:
    ImageContent:封装一张图片的内容,用于 MCP 工具返回图像结果。
    Property:描述一个MCP工具的输入参数(属性)。
    PropertyList:管理一组Property,代表一个工具的所有输入参数。
    McpTool:一个可被LLM调用的本地工具。
    McpServer:MCP服务的全局入口,负责协议解析与调度。它是一个单例。
  • main/protocol/protocol.cc, main/protocol/websocket_protocol.cc
    protocol是一个通信协议抽象类,websocket_protocol是实现类。

4.2 整体交互图

MCP在xiaozhi-esp32上的整体交互流程如下图:
MCP时序图

4.3 协议格式

消息结构格式如下所示:

{
  "session_id": "...", // 会话 ID
  "type": "mcp",       // 消息类型,固定为 "mcp"
  "payload": {         // JSON-RPC 2.0 负载
    "jsonrpc": "2.0",
    "method": "...",   // 方法名 (如 "initialize", "tools/list", "tools/call")
    "params": { ... }, // 方法参数 (对于 request)
    "id": ...,         // 请求 ID (对于 request 和 response)
    "result": { ... }, // 方法执行结果 (对于 success response)
    "error": { ... }   // 错误信息 (对于 error response)
  }
}

其中,payload部分是标准的JSON-RPC 2.0消息:
jsonrpc: 固定的字符串 “2.0”。
method: 要调用的方法名称 (对于 Request)。
params: 方法的参数,一个结构化值,通常为对象 (对于 Request)。
id: 请求的标识符,客户端发送请求时提供,服务器响应时原样返回。用于匹配请求和响应。
result: 方法成功执行时的结果 (对于 Success Response)。
error: 方法执行失败时的错误信息 (对于 Error Response)。

4.4 交互流程

MCP的交互主要是围绕客户端(后台 API)发现和调用设备上的“工具”(Tool)进行的。后台API相当于MCP的客户端,ESP32设备其实是MCP的服务器。

(1) 设备端初始化

esp32设备启动后,首先设备上电、初始化 Application,创建并初始化实现 Protocol 接口的WebSocket协议实例(WebsocketProtocol
参考源码application.cc的start函数:

void Application::Start() {
    // ...
    if (ota.HasMqttConfig()) {
        protocol_ = std::make_unique<MqttProtocol>();
    } else if (ota.HasWebsocketConfig()) {
        protocol_ = std::make_unique<WebsocketProtocol>();
    }
    // ...
}

(2) 建立WebSocket连接并发送"hello"消息

当设备需要开始语音会话时(如被用户唤醒),调用 OpenAudioChannel()函数,它会根据配置获取 WebSocket URL、设置若干请求头(Authorization, Protocol-Version, Device-Id, Client-Id)以及调用 Connect() 与服务器建立 WebSocket 连接。
参考源码websocket_protocol.cc的OpenAudioCHannel函数:

bool WebsocketProtocol::OpenAudioChannel() {
    // ...
    websocket_->SetHeader("Protocol-Version", std::to_string(version_).c_str());
    websocket_->SetHeader("Device-Id", SystemInfo::GetMacAddress().c_str());
    websocket_->SetHeader("Client-Id", Board::GetInstance().GetUuid().c_str());

    websocket_->OnData([this](const char* data, size_t len, bool binary) {
        // 接收数据处理
    }

        ESP_LOGI(TAG, "Connecting to websocket server: %s with version: %d", url.c_str(), version_);
    if (!websocket_->Connect(url.c_str())) {
        ESP_LOGE(TAG, "Failed to connect to websocket server");
        SetError(Lang::Strings::SERVER_NOT_CONNECTED);
        return false;
    }
    // 连接成功,发送hello消息
    // Send hello message to describe the client
    auto message = GetHelloMessage();
    if (!SendText(message)) {
        return false;
    }
    // ...
}

hello消息的格式参考:

{
    "type": "hello",
    "version": 1,
    "features": {
    "mcp": true
    },
    "transport": "websocket",
    "audio_params": {
    "format": "opus",
    "sample_rate": 16000,
    "channels": 1,
    "frame_duration": 60
    }
}
  • 其中 features 中的"mcp": true 表示支持 MCP 协议。
  • frame_duration 的值对应 OPUS_FRAME_DURATION_MS(例如 60ms)。

(3) 服务器回复 "hello"及初始化MCP

服务端收到hello后,会回复一个hello。此时通信正式建立。
同时,服务端确认设备支持MCP后, 会发送initialize初始化MCP会话。

服务器返回hello的消息格式如下:

{
    "type": "hello",
    "transport": "websocket",
    "session_id": "xxx",
    "audio_params": {
    "format": "opus",
    "sample_rate": 24000,
    "channels": 1,
    "frame_duration": 60
    }
}

初始化MCP会话消息格式如下:

{
  "jsonrpc": "2.0",
  "method": "initialize",
  "params": {
    "capabilities": {
      // 客户端能力,可选

      // 摄像头视觉相关
      "vision": {
        "url": "...", //摄像头: 图片处理地址(必须是http地址, 不是websocket地址)
        "token": "..." // url token
      }

      // ... 其他客户端能力
    }
  },
  "id": 1 // 请求 ID
}

解析MCP消息的方法在mcp_server.cc的ParseMessage,截取片段如下:

void McpServer::ParseMessage(const cJSON* json) {
    // ...
    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);
    } 
    // ...
}

如上面代码所示,在收到MCP初始化消息后,设备端立即做成响应。

(4)发现设备工具列表

接着,服务端会发送“tools/list”请求,消息格式如下:

{
  "jsonrpc": "2.0",
  "method": "tools/list",
  "params": {
    "cursor": "" // 用于分页,首次请求为空字符串
  },
  "id": 2 // 请求 ID
}

同样的,也是在ParseMessage方法中解析:

void McpServer::ParseMessage(const cJSON* json) {
    // ...
if (method_str == "tools/list") {
        std::string cursor_str = "";
        bool list_user_only_tools = false;
        if (params != nullptr) {
            auto cursor = cJSON_GetObjectItem(params, "cursor");
            if (cJSON_IsString(cursor)) {
                cursor_str = std::string(cursor->valuestring);
            }
            auto with_user_tools = cJSON_GetObjectItem(params, "withUserTools");
            if (cJSON_IsBool(with_user_tools)) {
                list_user_only_tools = with_user_tools->valueint == 1;
            }
        }
        GetToolsList(id_int, cursor_str, list_user_only_tools);
    } 
    // ...
}

设备端在获取工具列表后,返回结果。细节参考GetToolsList方法,以及McpTool类。当然,想要能够发现设备工具,前提是要将工具加入到列表中。请自行参考并调查如下函数:

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));
}

响应消息的格式如下:

{
  "jsonrpc": "2.0",
  "id": 2, // 匹配请求 ID
  "result": {
    "tools": [ // 工具对象列表
      {
        "name": "self.get_device_status",
        "description": "...",
        "inputSchema": { ... } // 参数 schema
      },
      {
        "name": "self.audio_speaker.set_volume",
        "description": "...",
        "inputSchema": { ... } // 参数 schema
      }
      // ... 更多工具
    ],
    "nextCursor": "..." // 如果列表很大需要分页,这里会包含下一个请求的 cursor 值
  }
}

(5) 调用设备工具

当后台需要执行设备上的某个具体功能时,就会调用tools/call方法来调用设备工具。
消息格式为:

{
  "jsonrpc": "2.0",
  "method": "tools/call",
  "params": {
    "name": "self.audio_speaker.set_volume", // 要调用的工具名称
    "arguments": {
      // 工具参数,对象格式
      "volume": 50 // 参数名及其值
    }
  },
  "id": 3 // 请求 ID
}

同样的,也是在ParseMessage方法中解析:

void McpServer::ParseMessage(const cJSON* json) {
    // ...
    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;
        }
        // 执行具体任务,比如上文中调整音量为50
        DoToolCall(id_int, std::string(tool_name->valuestring), tool_arguments);
    }
    // ...
}

返回消息结构为:

{
  "jsonrpc": "2.0",
  "id": 3, // 匹配请求 ID
  "result": {
    "content": [
      // 工具执行结果内容
      { "type": "text", "text": "true" } // 示例:set_volume 返回 bool
    ],
    "isError": false // 表示成功
  }
}

至此,一个正常的MCP交互流程就完成了。请结合时序图和代码一并来看,效果更加。

5、 写在最后

Model Context Protocol(MCP)作为一种面向大语言模型(LLM)的标准化工具调用协议,为LLM与物理世界的交互提供了通用接口。本文基于ESP32嵌入式平台上的实际实现,深入解读了MCP的运行机制,我们可以看出它们配合的几个特点:
轻量的协议适配
该版本的xiaozhi-esp32项目实现了MCP v2024-11-05 规范的核心方法(initialize、tools/list、tools/call),采用 JSON-RPC 2.0 作为传输格式,与WebSocket无缝集成。
硬件能力抽象化
将音频、屏幕、摄像头、系统信息等本地硬件功能封装为具名工具(如 self.audio_speaker.set_volume),形成统一的“设备能力 API”。工具描述包含自然语言说明与结构化输入 Schema,便于LLM理解与调用。
权限管控
引入User-Only工具机制,将敏感操作(如重启、固件升级)与普通控制指令隔离,确保LLM无法自主触发高危行为。

短短的一年,技术发展飞快!
可以预见的是,未来随着协议演进与硬件升级,嵌入式设备将成为LLM在现实世界中的“眼睛、耳朵与双手”,而且这些感官功能越来越丝滑,越来越强大。

参考:

1、https://github.com/modelcontextprotocol
2、https://github.com/78/xiaozhi-esp32
3、https://github.com/modelcontextprotocol/python-sdk
4、https://modelcontextprotocol.io/specification/versioning
5、https://modelcontextprotocol.io/specification/2025-11-25/changelog

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐