- 工具表:scan_devices 返回形状 data.devices=[{name,address,rssi}];
device_status 注明触发连接守候且刷新空闲计时(周期轮询阻止 idle 断开)
- 启动参数表新增 --allowed-hosts;Windows 部署段同步(含 421 说明)
- 连接行为节:重连覆盖面改为传输层异常全家
- spec 末尾加「实测与终审偏离记录」:5 码契约(DEVICE_DISCONNECTED 未 emit)、
mac 解析顺序修正(启动 --mac > 自动扫描记住,工具面无 mac 参数)、
异常面适配、熠管家 Windows 实测结论与 421 修复
Co-Authored-By: Claude <noreply@anthropic.com>
7.6 KiB
7.6 KiB
ppclock-mcp 设计:AI-native MCP 服务器(单层部署)
日期:2026-07-30 · 状态:已批准(用户免审,直接进入实现)
目标
把 ppclock SDK 的设备能力包装成 MCP 服务器,直接部署在蓝牙物理所在的 Windows 服务器上(单层,不经 bridge),供各 agent 平台调用。
- 传图、不同状态显示、时间核对等高层能力,AI-native 工具语义
- Windows 先行实测;Linux 保持代码兼容(bleak 跨平台),暂不实测
- Windows 部署与联调由 worker 与熠管家协作完成,用户不参与过程
架构
agent 平台(Claude Code/Desktop/其他)
│ stdio(本机 agent) 或 streamable HTTP(局域网 agent,bearer token)
▼
ppclock-mcp(运行在蓝牙所在服务器,单层,无 bridge)
├─ mcp_server.py FastMCP 装配:工具注册、传输启动、HTTP 鉴权、CLI 参数
├─ device_manager.py 连接管理器(方案 C)
└─ tools.py 高层工具薄包装:参数校验 + 统一输出契约
▼
ppclock SDK(via_local,bleak;Windows=WinRT / Linux=BlueZ)→ BLE → NRF- 设备
SDK 零改动:MCP 层只是 SDK 的消费者。
连接管理(方案 C:按需连接 + 空闲保持)
- 一把
asyncio.Lock串行所有设备操作(BLE 适配器独占,先到先得) - 首次操作守候连接:扫描/连接超时默认 90s(覆盖设备 30–60s 广播窗口)
- 操作完成后保持连接;空闲超过
--idle-timeout(默认 300s)自动断开, 设备恢复睡眠省电 - 操作中连接丢失 → 自动重连并重试一次,再失败才报错
- 配置退化:
--idle-timeout 0= 每次独立连接;很大值 ≈ 常驻连接
传输与鉴权
- 官方
mcpPython 包(FastMCP),一份实现双模:--transport stdio|http - HTTP = streamable HTTP,默认
--host 127.0.0.1 --port 8972(避开 bridge 8971) - 鉴权:环境变量
PPCLOCK_MCP_TOKEN;绑非回环地址时必须设置, 请求头Authorization: Bearer <token>,不符即 401 - stdio 模式无鉴权(进程隔离即边界)
安装形态
- 本仓库内:
pyproject.toml增加 optional extramcp = ["mcp>=1.2"] - 新入口脚本:
ppclock-mcp = ppclock.mcp_server:main - 基础 SDK 依赖不增加
工具面(13 个)
| 工具 | 参数 | 说明 |
|---|---|---|
scan_devices |
timeout=5.0 |
扫描 NRF- 设备 → [{name,address,rssi}] |
device_status |
— | 连接状态/绑定 MAC/设备 ID(连接中时)/空闲秒数 |
set_time |
tz=8.0 |
对时(默认北京) |
set_mode |
mode |
clock1-3/calendar1-3/image0-3/tricolor/mono |
toggle |
name |
invert/font/rotate180/hour_format/clock_color |
upload_image |
source, slot=0, algo="atkinson", mono=false, **adjust |
source = 服务器本地路径 或 base64(可带 data URI 前缀);返回 {bytes_bw,bytes_red,small,slot} |
render_template |
name, payload, slot=0, algo="atkinson" |
6 模板本地渲染上传 |
countdown |
date, mode="clock", prefix=None |
date 为 ISO YYYY-MM-DD |
countdown_off |
— | 关闭倒计时 |
calendar_text |
text |
日历中文文字并切日历模式一 |
set_sleep |
on, start_h, end_h |
休眠时段(0–23) |
clear_screen |
— | 刷屏 |
raw_send |
data_hex, channel="alt" |
逃生舱直发(hex 字符串) |
不暴露:OTA、激活、LUT、WiFi、轮播(高危或本固件 no-op)。
输出契约(与 CLI --json 一致)
成功 {"ok": true, "tool": <name>, "data": {...}};
失败 {"ok": false, "tool": <name>, "error": {"code", "message", "hint"}}。
错误码小集合:INVALID_PARAM / DEVICE_NOT_FOUND / CONNECT_TIMEOUT /
BLE_ERROR / DEVICE_DISCONNECTED / INTERNAL。DEVICE_NOT_FOUND 与
CONNECT_TIMEOUT 的 hint 说明设备睡眠与守候窗口机制。
数据流(典型)
- agent 调
upload_image(base64…)→ tools 校验/解码 → manager 取锁 - 无连接 →
PPClient.via_local(mac)守候连接(≤90s) - SDK 图像管线(裁剪 400×300→抖动→双平面打包)→ BLE 分块上传 → 返回字节统计
- 刷新空闲计时器;锁释放;空闲 300s 后自动断开
mac 解析顺序:工具参数 > 启动 --mac > 自动扫描首个 NRF-(并记住)。
测试
- 单元(无硬件):
- device_manager:fake transport 注入,覆盖串行锁、空闲超时断开、 掉线重连重试一次、守候超时错误
- tools:参数校验、base64/路径两种图源、错误契约形状
- MCP 协议层:FastMCP 内存 ClientSession 端到端调工具(fake transport)
- Windows 实测(熠管家协助):scan → set_time → upload_image → set_mode 目视确认上屏;HTTP 模式 token 鉴权正/反例;stdio 模式本机 agent 挂载
- Linux:仅保证导入与单元测试通过(bleak 分支兼容),不做硬件测试
部署(Windows,熠管家协作)
- 服务器准备 Python ≥3.10 venv;源码部署(git clone 或拷贝)
pip install -e ".[mcp]"(注意 bleak WinRT 后端,无需额外系统组件)- 设
PPCLOCK_MCP_TOKEN;启动ppclock-mcp --transport http --host 0.0.0.0 --port 8972 --mac AA:BB:… - 防火墙放行 8972(仅局域网);不动既有服务与 8971 bridge
- 测试期手动启动即可;稳定后再议是否注册为 Windows 服务(超出本期范围)
熠管家协作边界:worker 提供部署脚本/步骤与验证清单;熠管家提供 Windows 访问与执行;用户不参与。
文档与版本
- 版本 0.2.0;CHANGELOG、README(MCP 段)、llms.txt、docs/sdk/API.md 同步
- 新增 docs/sdk/MCP.md:工具参考 + agent 挂载示例(stdio/HTTP 双例)
明确不做(YAGNI)
- 图片 URL 拉取(保持离线;agent 可自行下载后走 base64)
- 多设备并发(单适配器串行已够;mac 参数可切换设备)
- MCP resources/prompts(本期只 tools)
- Windows 服务化、Linux 硬件实测(后续迭代)
实测与终审偏离记录(2026-07-30)
实现与终审后对原设计的偏离,逐条留痕:
- 错误码实为 5 码:
DEVICE_DISCONNECTED未 emit——操作中途掉线被 DeviceManager 的"重连+重试一次"吸收,重试仍败按BLE_ERROR上报;工具面 始终只看到最终失败,无法区分"中途掉过线"。契约保留 5 码:INVALID_PARAM/DEVICE_NOT_FOUND/CONNECT_TIMEOUT/BLE_ERROR/INTERNAL。 - mac 解析顺序修正:实际为"启动
--mac> 自动扫描首个NRF-前缀并记住", 工具面无 mac 参数(原设计的"工具参数 > …"未实现;多设备切换靠重启服务器 换--mac,归入 YAGNI 多设备并发)。 - 异常面适配真实硬件:全部测试 fake 原来只抛英文
transports.base. TransportError;真实 bleak 路径抛的是中文transports.local.TransportError(与 base 同名但互不继承)与未包装的BleakError(如Not connected)。 终审后 MCP 层捕获面扩为异常全家(两个 TransportError + BleakError + OSError- asyncio.TimeoutError),错误码按类型优先、消息(中英文)次之映射。
- 2026-07-30 熠管家 Windows 实测:stdio 全链路通过(scan → connect →
set_time → upload_image 15000+15000 字节 → set_mode image0;远程无法目视
确认上屏)。HTTP 模式 LAN Host 被 mcp DNS 重绑定防护 421——已修:新增
--allowed-hosts(逗号分隔 Host 白名单,TransportSecuritySettings实现,mcp 1.29.0 实测 API 与设计一致),Windows 部署文档同步更新。