逆向解析小鸿AI WS63 WebSocket协议并构建MCP Server实战

逆向解析小鸿AI WS63 WebSocket协议并构建MCP Server实战

1. 项目背景与核心价值:为什么需要关注这个协议?

最近在折腾一个智能家居项目,想把一个叫“小鸿AI WS63”的智能设备(我猜它可能是个带AI功能的传感器或者控制器)的数据,实时地接入到我自己的服务器应用里。设备厂商给的文档很简陋,只说支持WebSocket连接,但具体怎么连、数据格式是啥、心跳怎么维持,一概没提。这让我想起了之前集成各种IoT设备时踩过的坑:协议不透明,对接全靠猜,最后要么通信不稳定,要么数据解析出错。

于是,我决定把这次逆向和对接“小鸿AI WS63”与自建MCP Server(模型上下文协议服务器)的整个过程,以及最终梳理出的WebSocket通信协议细节,完整地记录下来。MCP Server是当前AI应用开发中的一个热门概念,它本质上是一个标准化的接口服务器,用于为大型语言模型(如GPT、Claude)提供工具调用和上下文数据。让设备数据通过WebSocket流入MCP Server,就能让AI模型实时感知到物理世界的变化,从而实现更智能的自动化决策。

这个协议详解的价值在于,它不仅仅是一份技术文档。对于开发者而言,它是一份可以直接“抄作业”的对接指南,能帮你省去大量抓包、猜格式、试错的时间。对于架构师,它展示了如何为一个私有协议设备构建稳定、可扩展的实时数据通道。整个过程涉及网络抓包、协议逆向、数据编解码、连接保活策略等一整套实战技能,无论你是做物联网、实时通信还是AI应用集成,都能从中找到共鸣和参考。

2. 逆向工程起点:从零捕获并解析原始通信流

当面对一个没有文档的通信协议时,第一步永远是抓包。我的目标设备“小鸿AI WS63”提供了一个Wi-Fi配置模式,使其能连接到我的本地网络。这样,我就能在同一个局域网内,用我的开发机进行流量监听。

2.1 工具选型与网络环境搭建

我选择了Wireshark作为主要的抓包工具,因为它对网络协议的解析能力最强。为了能抓到设备与服务器(假设是厂商云端)之间的通信,我需要让设备的流量经过我的电脑。有两种常见方案:

  1. 设置代理:在电脑上运行一个HTTP/WebSocket代理(如mitmproxy),并将设备的网关设置为我的电脑IP。但很多嵌入式设备不支持配置代理,此路不通。
  2. ARP欺骗/网关镜像:这是更通用的方法。我使用了arpspoof工具(需配合iptables),让我电脑成为设备和路由器之间的“中间人”。具体命令如下:
    # 启用IP转发 echo 1 > /proc/sys/net/ipv4/ip_forward # 对设备进行ARP欺骗,让它认为我的电脑是网关 arpspoof -i eth0 -t <设备IP> <网关IP> # 对网关进行ARP欺骗,让它认为我的电脑是设备 arpspoof -i eth0 -t <网关IP> <设备IP> # 将经过我电脑的WebSocket流量(通常端口443或自定义端口)重定向到本机的一个端口,方便Wireshark抓取 iptables -t nat -A PREROUTING -p tcp --dport 443 -j REDIRECT --to-port 8443
    然后,在Wireshark中监听eth0接口,并设置过滤条件tcp.port == 8443。这样,设备与真实服务器之间的TLS加密流量就被我“劫持”并解密(前提是我在电脑上安装了设备的CA证书,对于非加密WebSocket则更简单)。

2.2 首次连接与协议特征识别

启动抓包后,给设备上电。在Wireshark中,我很快看到了设备发起的TCP连接。通过跟踪TCP流(Follow -> TCP Stream),原始数据呈现出来。关键特征出现了:

  1. 一个标准的HTTP Upgrade请求:
    GET /ws/v1/data HTTP/1.1 Host: device-cloud.example.com:8883 Upgrade: websocket Connection: Upgrade Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ== Sec-WebSocket-Version: 13
    这证实了它使用WebSocket,并且路径是/ws/v1/data
  2. 服务器回复101 Switching Protocols,握手成功。
  3. 随后是二进制数据流。WebSocket帧的Payload部分是二进制的,无法直接阅读。这是逆向的核心难点。

2.3 二进制载荷解析:从乱码到结构

Wireshark可以解析WebSocket帧,但payload需要自己分析。我将一段时间内的二进制payload全部导出保存为raw.bin文件。接下来就是“猜”结构。根据经验,这类IoT设备数据帧通常包含:

  • 帧头(Header):固定的字节序列,如0xAA 0x55,用于标识帧开始。
  • 长度字段(Length):指示后续数据部分的长度。
  • 命令字/类型(Cmd/Type):标识这条消息是传感器数据、心跳、配置请求还是响应。
  • 数据载荷(Payload):具体的业务数据。
  • 校验和(Checksum):CRC8或CRC16,用于验证数据完整性。

我用十六进制编辑器打开raw.bin,并写了一个简单的Python脚本进行模式搜索:

import binascii with open('raw.bin', 'rb') as f: data = f.read() hex_str = binascii.hexlify(data).decode('utf-8') # 寻找可能的两字节帧头,如 AA55 for i in range(0, len(hex_str)-4, 2): if hex_str[i:i+4] == 'aa55': print(f"Possible header at byte offset {i//2}: {hex_str[i:i+20]}")

通过对比多个数据包,我发现了一个规律:每隔大约30秒,就会有一个非常短(例如8字节)的数据包交互。这极有可能是心跳包。锁定这些短包,对比它们的hex值,我假设了最简单的结构:[0xAA, 0x55, 0x01, 0x00, 0xXX, 0xXX],其中0x01可能是心跳命令,最后两字节是校验和。通过计算常见的CRC8/CRC16算法与最后两字节的匹配,我验证了校验算法是CRC16-CCITT(初始值0xFFFF)。

注意:逆向工程中,心跳包和错误响应包往往是突破口,因为它们结构相对固定且重复出现。先搞定它们,再攻克复杂的数据包。

3. “小鸿AI WS63” WebSocket协议帧格式全解构

经过对数十个数据包的比对、分类和验证,我最终还原出了“小鸿AI WS63”设备端使用的WebSocket二进制帧格式。这不是官方标准,而是基于实际通信逆向得出的事实标准

3.1 通用帧结构(Big-Endian)

所有上行(设备->服务器)和下行(服务器->设备)的数据帧,都遵循以下结构:

字节偏移字段名长度(字节)说明
0-1帧头(Header)2固定为0xAA55,标识一帧的开始。
2协议版本(Version)1当前协议版本,观察到的值为0x01
3命令字(Command)1定义帧的类型,是协议解析的核心。
4-5序列号(Seq)2请求/响应对的标识,用于匹配响应。通常由发起方设置,响应方回显。
6-7数据载荷长度(Length)2不包含帧头、版本、命令、序列号、长度、校验和这前10个字节。即后续Payload的实际字节数。
8-(8+N-1)数据载荷(Payload)N可变长度,由Length字段定义。内容格式根据Command不同而不同。
(8+N)-(8+N+1)校验和(Checksum)2帧头(0xAA55)到Payload最后一个字节的所有数据进行CRC16-CCITT计算的结果(初始值0xFFFF)。

关键点解析

  • 字节序:所有多字节字段(帧头、序列号、长度、校验和)均采用大端序(Big-Endian),即网络字节序。这在解析时至关重要,例如,长度字段0x00 0x10表示十进制16,而不是4096。
  • 长度计算Length = len(Payload)。整个帧的总长度是10 + Length字节。
  • 校验范围:校验和的计算不包含它自身。这是CRC校验的常规做法。

3.2 核心命令字(Command)枚举与含义

通过对交互流程的归类,我识别出以下关键命令:

命令值(Hex)方向名称描述
0x01设备 -> 服务器心跳请求(Heartbeat)设备定期发送,用于保活。Payload通常为空(Length=0)。
0x81服务器 -> 设备心跳响应(Heartbeat ACK)服务器对心跳的确认。Payload通常为空。
0x02设备 -> 服务器传感器数据上报(Sensor Data)设备上报其采集的数据(如温度、湿度、AI识别结果)。Payload结构复杂,见下文。
0x82服务器 -> 设备数据上报确认(Data ACK)服务器确认收到数据。Payload可包含服务器时间戳用于同步。
0x03服务器 -> 设备配置下发(Config Update)服务器向设备发送新的配置参数。
0x83设备 -> 服务器配置响应(Config Response)设备对配置下发的响应(成功/失败及原因)。
0x04设备 -> 服务器事件上报(Event Report)上报非周期性的AI事件,如检测到特定物体、异常报警。
0x84服务器 -> 设备事件响应(Event ACK)服务器确认收到事件。

3.3 关键Payload结构详解:以传感器数据(0x02)为例

这是最复杂的部分。设备上报的数据可能包含多种传感器信息。其Payload结构是一个TLV(Type-Length-Value)的嵌套结构

外层结构(设备级)

字节偏移字段长度说明
0-5设备ID6设备的唯一标识符,通常是MAC地址或烧录的ID。
6-9时间戳4设备端的Unix时间戳(秒级)。
10-11传感器数量(N)2本次上报包含的传感器数据块个数。
12-...传感器数据块列表可变包含N个传感器数据块,每个块是一个TLV结构。

内层结构(传感器数据块 - TLV): 每个传感器数据块由三部分组成:

  1. Type (1字节):传感器类型。例如:0x01=温度,0x02=湿度,0x10=AI识别结果(JSON字符串),0x11=电池电压。
  2. Length (2字节):后续Value字段的字节长度。
  3. Value (可变长度):传感器读数的具体值。其格式由Type决定:
    • 0x01(温度):2字节有符号整数,单位0.1°C。例如0x00 0x96= 150 => 15.0°C。
    • 0x02(湿度):1字节无符号整数,单位1%。例如0x45= 69%。
    • 0x10(AI结果):UTF-8编码的JSON字符串。例如{"object": "person", "confidence": 0.87, "bbox": [10,20,100,200]}
    • 0x11(电压):2字节无符号整数,单位mV。例如0x0B 0xB8= 3000 => 3.000V。

一个完整的数据上报帧解析示例(十六进制):

AA 55 01 02 00 01 00 1A // 帧头 |版本|命令|序列号 |长度(26) 00 00 00 00 00 01 5F 90 7B 2C 00 02 // 设备ID(00:00:00:00:00:01) |时间戳(0x5F907B2C) |传感器数量(2) 01 00 02 00 96 02 00 01 45 11 00 02 0B B8 // 传感器块1: Type=0x01(温度), Len=2, Value=0x0096(15.0°C) // 传感器块2: Type=0x02(湿度), Len=1, Value=0x45(69%) // 传感器块3: Type=0x11(电压), Len=2, Value=0x0BB8(3.000V) A1 7B // 校验和 (CRC16 of data from AA55 to ...B8)

4. 构建自有的MCP Server与协议适配层

了解了设备协议,下一步就是构建我们自己的MCP Server来接收和处理这些数据。我们的目标不是模拟原厂服务器,而是实现一个协议适配层,将设备的私有协议转换为MCP标准格式,供AI模型使用。

4.1 MCP Server核心概念与选型

MCP(Model Context Protocol)的核心思想是为AI模型提供一个统一的“工具箱”和“数据源”接口。一个MCP Server可以声明一系列Tools(函数)和Resources(数据),客户端(如Claude Desktop、Cursor)可以发现并调用它们。

我选择使用TypeScript/Node.js@modelcontextprotocol/sdk官方SDK来构建Server。原因如下:

  1. 生态成熟:Node.js的WebSocket库(如ws)非常强大,适合处理高并发连接。
  2. 开发效率:TypeScript的强类型有助于定义复杂的协议数据结构,减少错误。
  3. SDK支持:官方SDK封装了MCP的底层通信(JSON-RPC over STDIO/SSE),让我们专注于业务逻辑。

4.2 项目结构与核心模块设计

project/ ├── package.json ├── tsconfig.json ├── src/ │ ├── index.ts # 主入口,启动MCP Server和WebSocket Server │ ├── protocol/ # 协议解析层 │ │ ├── decoder.ts # 二进制帧解码器 │ │ ├── encoder.ts # 二进制帧编码器 │ │ └── types.ts # 协议相关的类型定义(命令、传感器类型等) │ ├── device-manager.ts # 设备连接管理、状态维护 │ ├── mcp-handlers.ts # MCP Tools和Resources的实现 │ └── ws-server.ts # 专用于小鸿设备的WebSocket服务器 └── ...

4.3 WebSocket服务器实现与协议解码

ws-server.ts中,我们使用ws库创建一个WebSocket服务器,监听特定端口(如8888)。

// ws-server.ts import WebSocket, { WebSocketServer } from 'ws'; import { decodeFrame, isHeartbeat, parseSensorData } from './protocol/decoder'; import { encodeHeartbeatAck } from './protocol/encoder'; import { DeviceManager } from './device-manager'; export function createDeviceWebSocketServer(port: number, deviceManager: DeviceManager) { const wss = new WebSocketServer({ port }); wss.on('connection', (ws: WebSocket, request) => { const clientIp = request.socket.remoteAddress; console.log(`新的设备连接来自: ${clientIp}`); let deviceId: string | null = null; ws.on('message', (data: Buffer) => { try { // 1. 解码二进制帧 const frame = decodeFrame(data); console.log(`收到命令: 0x${frame.command.toString(16).padStart(2, '0')}, 序列号: ${frame.seq}`); // 2. 处理心跳 if (isHeartbeat(frame)) { const ackFrame = encodeHeartbeatAck(frame.seq); ws.send(ackFrame); console.log(`已发送心跳响应给设备 ${deviceId}`); return; } // 3. 处理数据上报 (0x02) if (frame.command === 0x02) { const sensorReport = parseSensorData(frame.payload); deviceId = sensorReport.deviceId; // 从数据中提取设备ID // 将数据交给设备管理器处理 deviceManager.updateDeviceData(deviceId, { ...sensorReport, lastSeen: Date.now(), wsConnection: ws }); // 发送确认帧 (可选,根据协议需要) // const ackFrame = encodeDataAck(frame.seq, Date.now()); // ws.send(ackFrame); } // 4. 处理其他命令... } catch (error) { console.error('解析设备数据帧失败:', error); // 可以考虑发送一个错误响应帧,或者直接关闭连接 ws.close(1002, 'Protocol error'); } }); ws.on('close', () => { console.log(`设备连接关闭: ${deviceId || clientIp}`); if (deviceId) { deviceManager.removeDevice(deviceId); } }); ws.on('error', (error) => { console.error(`WebSocket错误: ${error}`); }); }); console.log(`小鸿设备WebSocket服务器已启动在端口 ${port}`); return wss; }

decoder.ts是核心,实现了帧的验证和解码:

// protocol/decoder.ts import crc from 'crc'; import { SensorDataReport, DeviceFrame } from './types'; export function decodeFrame(buffer: Buffer): DeviceFrame { // 1. 基础长度检查 if (buffer.length < 10) { throw new Error('帧长度过短'); } // 2. 检查帧头 if (buffer.readUInt16BE(0) !== 0xaa55) { throw new Error('无效的帧头'); } // 3. 提取字段 const version = buffer.readUInt8(2); const command = buffer.readUInt8(3); const seq = buffer.readUInt16BE(4); const length = buffer.readUInt16BE(6); // 4. 校验长度字段是否与实际Buffer长度一致 if (buffer.length !== 10 + length) { throw new Error(`长度字段不匹配。预期: ${10 + length}, 实际: ${buffer.length}`); } // 5. 计算并校验CRC const expectedChecksum = buffer.readUInt16BE(8 + length); const dataToCheck = buffer.slice(0, 8 + length); // 从帧头到Payload结束 const actualChecksum = crc.crc16ccitt(dataToCheck, 0xffff); if (expectedChecksum !== actualChecksum) { throw new Error(`CRC校验失败。预期: 0x${expectedChecksum.toString(16)}, 实际: 0x${actualChecksum.toString(16)}`); } // 6. 提取Payload const payload = buffer.slice(8, 8 + length); return { version, command, seq, length, payload }; } export function parseSensorData(payload: Buffer): SensorDataReport { // 解析3.3节描述的TLV结构 const deviceId = payload.slice(0, 6).toString('hex'); // 转为MAC格式 const timestamp = payload.readUInt32BE(6); const sensorCount = payload.readUInt16BE(10); const sensors = []; let offset = 12; for (let i = 0; i < sensorCount; i++) { const type = payload.readUInt8(offset); const len = payload.readUInt16BE(offset + 1); const valueBuffer = payload.slice(offset + 3, offset + 3 + len); // 根据type解析valueBuffer let value: any; switch (type) { case 0x01: // 温度 value = valueBuffer.readInt16BE(0) / 10.0; break; case 0x02: // 湿度 value = valueBuffer.readUInt8(0); break; case 0x10: // AI结果 (JSON字符串) value = JSON.parse(valueBuffer.toString('utf8')); break; // ... 其他类型 default: value = valueBuffer; } sensors.push({ type, value }); offset += 3 + len; } return { deviceId, timestamp, sensors }; }

5. 将设备数据暴露为MCP资源与工具

设备数据接入后,我们需要通过MCP协议将其暴露出去。这主要通过实现ResourcesTools来完成。

5.1 定义MCP资源(Resources)

资源代表可读的数据源。我们可以将每个设备的最新状态定义为一个资源。

// mcp-handlers.ts import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { DeviceManager } from './device-manager'; export function setupMcpResources(server: Server, deviceManager: DeviceManager) { // 声明一个资源列表,例如所有设备 server.setRequestHandler(ListResourcesRequestSchema, async () => { const devices = deviceManager.getAllDevices(); const resources: Resource[] = devices.map(device => ({ uri: `device://${device.id}/state`, name: `设备 ${device.id} 的实时状态`, description: `包含传感器读数、在线状态等信息`, mimeType: 'application/json' })); // 还可以声明一个汇总资源 resources.push({ uri: `device://summary`, name: `所有设备状态汇总`, description: `所有已连接设备的快照`, mimeType: 'application/json' }); return { resources }; }); // 处理资源读取请求 server.setRequestHandler(ReadResourceRequestSchema, async (request) => { const url = new URL(request.params.uri); if (url.pathname === '/summary') { const devices = deviceManager.getAllDevices(); return { contents: [{ uri: request.params.uri, mimeType: 'application/json', text: JSON.stringify({ timestamp: Date.now(), onlineCount: devices.length, devices: devices.map(d => ({ id: d.id, lastSeen: d.lastSeen, data: d.latestData // 最新传感器数据 })) }, null, 2) }] }; } // 匹配 device://{deviceId}/state const match = url.pathname.match(/^\/([^\/]+)\/state$/); if (match) { const deviceId = match[1]; const device = deviceManager.getDevice(deviceId); if (!device) { throw new Error(`设备 ${deviceId} 未找到`); } return { contents: [{ uri: request.params.uri, mimeType: 'application/json', text: JSON.stringify(device.latestData, null, 2) }] }; } throw new Error(`未知资源: ${request.params.uri}`); }); }

5.2 定义MCP工具(Tools)

工具代表可执行的函数。我们可以提供一些控制或查询工具。

// mcp-handlers.ts export function setupMcpTools(server: Server, deviceManager: DeviceManager) { server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools: [ { name: 'get_device_history', description: '获取指定设备在过去一段时间内的历史传感器数据', inputSchema: { type: 'object', properties: { deviceId: { type: 'string', description: '设备ID (如MAC地址)' }, durationMinutes: { type: 'number', description: '查询最近多少分钟的数据', default: 60 } }, required: ['deviceId'] } }, { name: 'send_device_command', description: '向指定设备发送控制命令(如下发配置)', inputSchema: { type: 'object', properties: { deviceId: { type: 'string' }, command: { type: 'string', enum: ['reboot', 'get_config', 'set_report_interval'], description: '要执行的命令' }, params: { type: 'object', description: '命令参数' } }, required: ['deviceId', 'command'] } } ] }; }); server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; if (name === 'get_device_history') { const { deviceId, durationMinutes = 60 } = args as any; // 从设备管理器或数据库中查询历史数据 const history = deviceManager.getDeviceHistory(deviceId, durationMinutes); return { content: [{ type: 'text', text: `设备 ${deviceId} 最近${durationMinutes}分钟的历史数据:\n${JSON.stringify(history, null, 2)}` }] }; } if (name === 'send_device_command') { const { deviceId, command, params } = args as any; const device = deviceManager.getDevice(deviceId); if (!device || !device.wsConnection) { throw new Error(`设备 ${deviceId} 未连接`); } // 根据命令编码对应的下行帧 const commandFrame = encodeControlCommand(deviceId, command, params); device.wsConnection.send(commandFrame); return { content: [{ type: 'text', text: `已向设备 ${deviceId} 发送命令: ${command}` }] }; } throw new Error(`未知工具: ${name}`); }); }

5.3 主程序入口与集成

最后,在index.ts中将所有部分串联起来:

// index.ts import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { DeviceManager } from './device-manager'; import { createDeviceWebSocketServer } from './ws-server'; import { setupMcpResources, setupMcpTools } from './mcp-handlers'; async function main() { // 1. 初始化设备管理器(用于在内存中维护设备状态) const deviceManager = new DeviceManager(); // 2. 启动面向小鸿设备的WebSocket服务器 const wsServer = createDeviceWebSocketServer(8888, deviceManager); // 3. 创建并启动MCP Server const server = new Server( { name: 'xiaohong-ai-mcp-server', version: '1.0.0', }, { capabilities: { resources: {}, // 启用资源功能 tools: {}, // 启用工具功能 }, } ); // 4. 设置MCP请求处理器 setupMcpResources(server, deviceManager); setupMcpTools(server, deviceManager); // 5. 使用Stdio传输层启动(这是最通用的方式,被Claude Desktop等客户端支持) const transport = new StdioServerTransport(); await server.connect(transport); console.error('小鸿AI MCP Server 已通过Stdio启动'); } main().catch((error) => { console.error('服务器启动失败:', error); process.exit(1); });

6. 实战部署、测试与排错指南

理论完成,代码就绪,接下来就是真刀真枪的测试和部署。

6.1 模拟设备测试

在真实设备接入前,编写一个模拟客户端进行全链路测试至关重要。

# simulator.py - 模拟小鸿AI WS63设备 import asyncio import websockets import struct import crc16 import json import time async def simulate_device(): uri = "ws://localhost:8888/ws/v1/data" # 连接到我们自建的服务器 async with websockets.connect(uri) as websocket: device_id = bytes([0x00, 0x11, 0x22, 0x33, 0x44, 0x55]) seq = 1 while True: # 1. 发送心跳 heartbeat_frame = build_frame(0x01, seq, b'') await websocket.send(heartbeat_frame) print(f"发送心跳, seq={seq}") seq += 1 # 等待心跳响应(可选) # try: # response = await asyncio.wait_for(websocket.recv(), timeout=2.0) # print(f"收到响应: {response.hex()}") # except asyncio.TimeoutError: # print("心跳响应超时") # 2. 每隔一段时间发送传感器数据 await asyncio.sleep(30) # 模拟30秒上报间隔 sensor_data = build_sensor_payload(device_id, [ (0x01, struct.pack('>h', 245)), # 温度24.5°C (0x02, bytes([65])), # 湿度65% (0x10, json.dumps({"object": "cat", "confidence": 0.92}).encode('utf-8')) ]) data_frame = build_frame(0x02, seq, sensor_data) await websocket.send(data_frame) print(f"发送传感器数据, seq={seq}") seq += 1 await asyncio.sleep(30) def build_frame(command, seq, payload): """构建协议帧""" header = b'\xaa\x55' version = b'\x01' cmd = bytes([command]) seq_bytes = seq.to_bytes(2, 'big') length = len(payload).to_bytes(2, 'big') # 计算CRC(从header到payload) data_for_crc = header + version + cmd + seq_bytes + length + payload checksum = crc16.crc16xmodem(data_for_crc, 0xffff) checksum_bytes = checksum.to_bytes(2, 'big') return data_for_crc + checksum_bytes def build_sensor_payload(device_id, sensor_list): """构建传感器数据Payload""" timestamp = int(time.time()).to_bytes(4, 'big') count = len(sensor_list).to_bytes(2, 'big') payload = device_id + timestamp + count for sensor_type, value_bytes in sensor_list: payload += bytes([sensor_type]) + len(value_bytes).to_bytes(2, 'big') + value_bytes return payload asyncio.run(simulate_device())

运行模拟器,观察MCP Server的日志,确认连接建立、数据解析、资源更新都正常。

6.2 连接真实设备

将“小鸿AI WS63”设备配置到与MCP Server同一局域网,并修改其服务器地址为MCP Server的IP和端口(8888)。这通常需要通过设备厂商的配网APP或本地配置页面完成。设备连接后,在服务器日志中应看到新的连接和源源不断的数据上报。

6.3 使用MCP客户端测试

启动一个支持MCP的客户端,如Claude Desktop。在其设置中添加自定义MCP Server,配置为我们的服务器(例如通过Stdio调用我们的Node.js脚本)。连接成功后,你就可以在Claude的对话中直接使用我们定义的工具和资源了。

例如,你可以问Claude:“/get_device_historydeviceId=00:11:22:33:44:55 durationMinutes=10”,它会调用我们的工具并返回格式化后的数据。或者,你可以让它“读取一下所有设备的当前状态”,它会通过device://summary资源获取信息。

6.4 常见问题与排错

  1. 连接失败:检查防火墙是否开放了8888端口。检查设备端配置的服务器地址和端口是否正确。在服务器端使用netstat -an | grep 8888查看端口监听状态。
  2. 数据解析错误:首先检查日志中的CRC错误。如果CRC频繁失败,可能是字节序假设错误(大端/小端),或者帧头判断有误。用Wireshark抓取MCP Server收到的原始数据,与设备直连原厂服务器的数据进行比对。
  3. MCP客户端无法发现工具/资源:确保MCP Server通过Stdio正确启动,并且客户端配置的command能正确启动你的脚本。检查服务器日志是否有初始化错误。MCP协议要求Server在启动后立即发送initialize请求,确认你的SDK处理正确。
  4. 连接不稳定,频繁断开:检查心跳机制。确保你的服务器能正确响应0x01心跳请求并回复0x81。检查设备的心跳间隔,如果服务器在超时时间内未收到任何数据(心跳或业务数据),应主动断开连接并清理资源。
  5. 性能问题:当连接数百个设备时,需要考虑优化。ws库本身性能很好,瓶颈可能在业务逻辑。可以将设备状态更新改为异步非阻塞操作,考虑使用Redis等内存数据库存储设备状态,而非全部放在Node.js内存中。

整个对接过程,从抓包逆向到最终实现一个功能完整的MCP Server,最耗时的部分往往是协议细节的确认和边界情况的处理。这份详解希望能为你提供一个清晰的路线图和可复用的代码框架,让你在对接类似私有协议物联网设备时,能少走弯路,快速构建起连接物理世界与AI模型的可靠桥梁。