ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Stack-chan 的 MCP 客户端服务测试:在 Moddable 上验证 Model Context Protocol 客户端全流程

Stack-chan 的 MCP 客户端服务测试:在 Moddable 上验证 Model Context Protocol 客户端全流程
  • 嵌入式
  • 物联网
  • 智能硬件
  • 机器人
  • 硬件开发
  • 前端
  • AI 应用

【免费下载链接】stack-chan

A JavaScript-driven M5Stack-embedded super-kawaii robot.

项目地址:https://gitcode.com/gh_mirrors/sta/stack-chan
点击查看免费下载

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)将本次测试概括为三个步骤:

  1. 启动一个带测试工具的 dummy MCP 服务器;
  2. 创建一个 MCP 客户端并连接该服务器;
  3. 验证一系列客户端操作,包括:
    • 初始化连接(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.

项目地址:https://gitcode.com/gh_mirrors/sta/stack-chan
点击查看免费下载
上一篇:FIRRTL安全分析:硬件编译器中的安全漏洞与防护
下一篇:Bridgy完整指南:掌握反向反馈(Backfeed)技术,让社交互动回归你的网站

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进