- 嵌入式
- 物联网
- 智能硬件
- 机器人
- 硬件开发
- 前端
- AI 应用
【免费下载链接】stack-chan
A JavaScript-driven M5Stack-embedded super-kawaii robot.
Stack-chan 是一个由 JavaScript 驱动的 M5Stack 嵌入式机器人,其固件在 Moddable SDK 之上实现了 Model Context Protocol(MCP)能力:既可以在设备上作为 MCP 服务器向 AI 客户端暴露工具,也可以作为 MCP 客户端调用远端服务器的工具。本篇以firmware/host/modules/connectivity/mcp-client/__tests__/mcp-client-service/下的测试文档为骨架,完整讲解如何通过"本地 dummy 服务器 + 真实 MCP 客户端"的方式验证MCPClientService的初始化、工具列举、工具调用、错误处理与重置重连等核心行为,并结合仓库源码说明其底层协议实现与上层集成方式。读完本文,你将掌握该测试的启动方法、执行流程、预期输出,以及MCPClientService各公共 API 的语义与使用限制。
测试目标:一条端到端的 MCP 客户端验证链路
测试文档(README)将本次测试概括为三个步骤:
- 启动一个带测试工具的 dummy MCP 服务器;
- 创建一个 MCP 客户端并连接该服务器;
- 验证一系列客户端操作,包括:
- 初始化连接(Initialize connection)
- 列举可用工具(List available tools)
- 以不同参数调用工具(Call tools with different parameters)
- 对非法请求的错误处理(Error handling for invalid requests)
- 重置与重新初始化(Reset and re-initialization)
这套链路的最大价值在于:它不依赖任何外部服务,在单台 Linux 主机上即可闭环验证客户端与服务器两端的协议行为。dummy 服务器由仓库内的MCPServerService实现(mcp-server.ts),客户端则直接使用被测对象MCPClientService(mcp-client.ts),两者通过http://localhost:8081/mcp进行 Streamable HTTP 传输,属于"以真验证真"的集成级测试,而非简单的单元 mock。
Dummy 服务器与三个测试工具
测试入口 mcp-client-service.test.ts 首先定义了三个Tool对象并注入MCPServerService:
| 工具名 | 说明 | 参数 | 处理逻辑 |
|---|---|---|---|
echo | 回显输入消息 | message(string,必填) | 返回Echo: <message> |
add | 两数相加 | a、b(number,均必填) | 校验数字合法性后返回<a> + <b> = <和> |
get_status | 返回服务器状态 | 无 | 返回Server is running normally |
这三个工具的设计非常有代表性:echo覆盖字符串参数,add覆盖数值参数并展示参数校验失败时的异常路径(Number.isNaN检查),get_status覆盖无参数工具调用。其中add在参数非法时会抛出Both arguments must be valid numbers,该异常会被服务器端#handleToolsCall捕获并包装为Tool execution failed: ...,最终以 JSON-RPC error 形式返回客户端。
服务器以如下方式启动(测试代码 L74-L78):
const _server = new MCPServerService({ port: 8081, // 使用不同端口以避免冲突 token: 'test-token', tools: testTools, })注意测试特意选用8081端口,注释明确说明是为了与默认端口8080错开、避免冲突。随后await asyncWait(1000)等待服务器完成启动,再创建客户端:
const client = new MCPClientService({ url: 'http://localhost:8081/mcp', token: 'test-token', })客户端与服务器共用同一个test-token,对应服务器端POST /mcp的Authorization: Bearer <token>鉴权要求。
运行方式:环境准备与执行命令
文档给出的 Linux 运行方式为:
source ~/.local/share/xs-dev-export.sh npm run test:moddable -- ./host/modules/connectivity/mcp-client/__tests__/mcp-client-service/manifest.test.json其中第一条命令导入 Moddable SDK 的xs-dev环境变量(MODDABLE路径等),这是后续mcconfig构建与mcsim模拟器运行的前提;第二条命令将测试清单文件作为参数传给固件模块测试运行器。在 package.json 中,test:moddable对应脚本为node scripts/run-module-tests.js。该运行器(run-module-tests.js)会:
- 递归扫描参数指定的
manifest.test.json,默认根目录为host/app、host/modules、mods/examples; - 用
mcconfig -d -m -p lin -t build将清单构建为mc.so; - 通过
mcsim模拟器运行,并借助 xsbug 日志服务监听输出; - 以日志中出现
<log>ok</log>判定测试通过(对应测试文件末尾的trace('ok\n')),超时或出现XS abort、stack overflow等致命标记则判定失败。
因此该测试本质上是在 Linux 桌面模拟器(lin平台)上完整运行 Moddable 的 fetch、HTTP 服务与 Timer 能力,而不是在真实 ESP32 硬件上执行。
测试清单 manifest.test.json 拆解
manifest.test.json 定义了该测试的构建依赖与平台配置:
{ "include": [ "$(MODDABLE)/examples/manifest_base.json", "$(MODDABLE)/examples/manifest_net.json", "$(MODDABLE)/examples/manifest_typings.json", "$(MODDABLE)/examples/io/listener/listen/manifest_listen.json", "../../../../../app/manifest_typings.json", "../../manifest.json", "../../../mcp-server/manifest.json", "../../../../testing/manifest.json" ], "modules": { "main": "./mcp-client-service.test" }, "platforms": { "lin": { "config": { "network": { "interface": "ethernet" } } } } }各 include 项的作用:
manifest_base.json/manifest_net.json:Moddable 基础运行库与网络栈;manifest_typings.json:TypeScript 类型定义;manifest_listen.json:监听相关能力;app/manifest_typings.json:应用层类型定义;../../manifest.json:即 mcp-client 模块清单,它 include 了$(MODDABLE)/examples/io/tcp/fetch/manifest_fetch.json,并将模块名mcp-client映射到./mcp-client,同时preload该模块;../../../mcp-server/manifest.json:引入MCPServerService实现,供测试搭建 dummy 服务器;../../../../testing/manifest.json:提供testing/assert等测试基础设施。
platforms.lin.config.network.interface = "ethernet"显式指定模拟器走以太网接口,保证 fetch 与 HTTP 服务在lin平台下可用。这是该测试能够在一个进程内同时起服务端与客户端的关键。
六个测试步骤与预期输出
测试主体用trace()打印每一步的通过(✓)或失败(✗)状态,对应文档"Expected Output"一节描述的观察点。六个步骤依次为:
Test 1:Initialize client(L92-L102) 调用client.initialize(),成功后打印协议版本(Protocol Version)、服务器名称与版本(Server Name/Server Version)。客户端发送的初始化请求参数(见 mcp-client.ts L124-L133)为:
{ "protocolVersion": "2024-11-05", "capabilities": { "tools": {} }, "clientInfo": { "name": "stack-chan-mcp-client", "version": "1.0.0" } }服务器端#handleInitialize则返回protocolVersion: '2024-11-05'、capabilities.tools以及serverInfo: { name: 'stack-chan-mcp-server', version: '1.0.0' }。
Test 2:List tools(L108-L119) 调用client.listTools(),遍历返回结果逐行打印- <name>: <description>,预期能看到echo、add、get_status三个工具。
Test 3:Call echo tool(L124-L131)client.callTool('echo', { message: 'Hello Stack-chan!' }),预期结果为Echo: Hello Stack-chan!。
Test 4:Call add tool(L136-L143)client.callTool('add', { a: 42, b: 58 }),预期结果为42 + 58 = 100。
Test 5:Call get_status tool(L148-L155)client.callTool('get_status'),预期结果为Server is running normally。
Test 6:Re-initialize after reset(L160-L172) 先调用client.reset(),再重新initialize(),随后再次调用echo验证工具在重置后依然可用。这一步验证的是"会话断开后可重新建立连接"的状态恢复能力。
步骤之间穿插await asyncWait(30),注释说明是为了给垃圾回收留出时间。整体流程覆盖了文档声称的"初始化、工具列举、不同参数的工具调用、错误处理、重置与重新初始化"全部要点。
深入MCPClientService:协议与状态机细节
MCPClientService是本次测试的被测对象,实现于 mcp-client.ts。其公共 API 与内部状态机如下:
配置项 MCPClientConfig
export interface MCPClientConfig { url: string // MCP 服务端地址,如 http://localhost:8081/mcp token?: string // Bearer token,可选 }构造函数会去除url末尾多余的/(L112),避免拼出//mcp之类的路径。
公共方法语义
| 方法 | 行为 | 关键实现细节 |
|---|---|---|
initialize() | 与服务器建立连接 | 已初始化时重复调用会抛出Client is already initialized;成功后将#initialized置为true并返回InitializeResult(协议版本、能力、服务器信息) |
listTools() | 获取可用工具列表 | 内部先调用#ensureInitialized(),未初始化时抛Client not initialized. Call initialize() first. |
callTool(name, args?) | 执行指定工具 | 同样要求先初始化;arguments缺省时发送空对象{} |
isInitialized() | 查询是否已初始化 | 供上层在接入客户端前做可用性判断 |
reset() | 清空客户端状态 | 将#initialized置false、请求 ID 归零、清空#sessionId |
底层请求发送(#sendRequest)
每次请求都构造标准 JSON-RPC 2.0 报文(L195-L203):
{ "jsonrpc": "2.0", "id": 1, "method": "initialize", "params": { ... } }请求 ID 由#generateRequestId()自增产生,确保同一客户端实例内消息可关联。HTTP 头部的构造体现了对 MCP 规范的遵循(L207-L221):
Content-Type: application/json;Accept: application/json, text/event-stream(注释明确说明客户端必须同时接受两种内容类型,以兼容服务器的 JSON 或 SSE 响应);- 配置了 token 时附加
Authorization: Bearer <token>; - 初始化之后的所有请求附带
Mcp-Session-Id头(在initialize响应的mcp-session-id响应头中提取并保存)。
响应侧做了三层校验:HTTP 状态码非 2xx 时抛HTTP error: <status> <statusText>;JSON 响应必须满足jsonrpc === '2.0'且id与请求 ID 一致,否则分别抛Invalid JSON-RPC response/Response ID mismatch;JSON-RPC 层出现error字段时抛出包含error.message的异常。这些校验正是"对非法请求的错误处理"能产生可观测输出的实现基础。
SSE 响应的兼容处理(#handleSSEResponse)
当服务器以text/event-stream响应时,客户端逐行解析 SSE 帧,仅关心data:前缀的数据行,遇到[DONE]结束标记或找到id与当前请求匹配的 JSON 载荷即停止(mcp-client.ts L271-L311)。注释指出,由于 Moddable 环境缺乏完善的流式支持,当前实现采用"退化为整体文本解析"的方式,属于适配嵌入式环境的务实取舍。
服务器侧支撑:鉴权、路由与工具分发
测试能闭环得益于仓库同时提供了服务器实现。MCPServerService(mcp-server.ts)暴露两个 HTTP 端点:GET /health(免鉴权,返回{"status":"ok"})与POST /mcp(必须携带 Bearer token)。POST /mcp内部按initialize、tools/list、tools/call三个方法名分发(L274-L286),未知方法返回 JSON-RPC 标准错误-32601 Method not found,JSON 解析失败返回-32700 Parse error。
Bearer token 的校验实现在 mcp-auth.ts:authorizeMCPRequest会依次检查"是否配置了 token"(missing-token-configuration)、"是否携带 Authorization 头"(missing-authorization)、"头是否符合Bearer <token>格式且值一致"(invalid-authorization)。值得注意的是,token 比较使用constantTimeEqual常量时间比较实现,避免时序侧信道;token 未配置时,POST /mcp将被一律拒绝。服务器构造时的 token 优先级为:构造参数token>Preference中的mcp.token(通过Preference.get(DOMAIN.mcp, 'token')读取)。
上层集成:MCP 客户端如何变成机器人可用的工具
测试文档虽然只覆盖客户端自身行为,但MCPClientService在 Stack-chan 固件中的实际用途可以追溯两处源码:
- 对话模块桥接:mcp-tools.ts 的
createMCPChatTools(clients)会遍历传入的MCPClientService[],逐个listTools(),并把远端工具转换为本地ChatTool(execute内部调用client.callTool并将content[].text归一化为字符串)。其测试 mcp-chat-tools.test.ts 验证了"远端 MCP 工具被转成 ChatTool 且调用结果被正确归一化"。 - ChatGPT 对话示例:dialogue-chatgpt.ts 的
ChatGPTDialogue接受mcpClients?: MCPClientService[]构造参数,构造时用isInitialized()过滤未就绪的客户端,并在生成工具表时把每个客户端的tools/list结果注册为可执行工具(执行时转调mcpClient.callTool)。
这两处集成印证了客户端测试的价值:只有initialize/listTools/callTool/isInitialized/reset这套状态机行为正确,上层对话系统才能把机器人自己的工具安全地暴露给 AI 模型调用。
排错与延伸验证建议
如果测试未通过,可优先从以下几方面排查:
MODDABLE未设置:source ~/.local/share/xs-dev-export.sh未执行或路径与本地xs-dev安装位置不一致,run-module-tests.js会直接报错退出;- 端口冲突:dummy 服务器固定使用
8081,若本地已有进程占用会触发#startServer的 failed 状态,测试输出中将看不到正常的=== MCP Client Service Test ===流程; - token 不一致:客户端与服务器 token 不匹配时,
POST /mcp返回401 Unauthorized,initialize会抛 HTTP 错误; - 重复初始化:在未
reset()的情况下再次调用initialize()会抛Client is already initialized,这正是 Test 6 先reset()再初始化所要规避的场景。
延伸验证时,也可以参考服务器端测试 mcp-server-service 测试 提供的 curl 命令,对独立启动的 MCP 服务器直接发起initialize、tools/list、tools/call请求,从 HTTP 层面交叉验证协议行为。
小结
mcp-client-service测试用约 180 行 TypeScript 在一个进程内搭建了"服务器 + 客户端"的完整 MCP 闭环,覆盖了MCPClientService的全部公共 API 与关键状态流转:初始化握手(携带协议版本与客户端信息)、工具列举、三种参数形态的工具调用、非法请求的错误传播,以及reset()后的重新连接。结合 mcp-client.ts 的源码可以看到,这层实现以 JSON-RPC 2.0 为消息格式、以 Streamable HTTP 为传输,并针对 Moddable 嵌入式环境做了 SSE 降级解析与轻量状态管理等适配——这正是 Stack-chan 得以在资源受限设备上参与 MCP 生态的工程基础。
- 嵌入式
- 物联网
- 智能硬件
- 机器人
- 硬件开发
- 前端
- AI 应用
【免费下载链接】stack-chan
A JavaScript-driven M5Stack-embedded super-kawaii robot.
相关推荐
awesome-mcp-clients:开源Model Context Protocol客户端集锦
awesome mcp clients:开源Model Context Protocol客户端集锦 项目介绍 在当今AI技术飞速发展的背景下,Model Con
文档知识库QtScrcpy 手机投屏键鼠控制入门:无线连接+按键映射一次到位
QtScrcpy 手机投屏键鼠控制入门:无线连接+按键映射一次到位 在把手机接上电脑之前,帮人查手机只能盯着他的屏幕喊"往左滑一点"。QtScrcpy 改变的就
桌面应用音视频Model Context Protocol开发者指南:从零开始构建MCP客户端
Model Context Protocol开发者指南:从零开始构建MCP客户端 Model Context Protocol(MCP)是一个开源协议,它让AI
人工智能AI Agent工具调用
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考