Files
qianbian/docs/superpowers/specs/2026-07-30-ppclock-mcp-design.md
T

8.1 KiB
Raw Blame History

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 = 每次独立连接;很大值 ≈ 常驻连接

传输与鉴权

  • 官方 mcp Python 包(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 extra mcp = ["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 说明设备睡眠与守候窗口机制。

数据流(典型)

  1. agent 调 upload_image(base64…) → tools 校验/解码 → manager 取锁
  2. 无连接 → PPClient.via_local(mac) 守候连接(≤90s)
  3. SDK 图像管线(裁剪 400×300→抖动→双平面打包)→ BLE 分块上传 → 返回字节统计
  4. 刷新空闲计时器;锁释放;空闲 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,熠管家协作)

  1. 服务器准备 Python ≥3.10 venv;源码部署(git clone 或拷贝)
  2. pip install -e ".[mcp]"(注意 bleak WinRT 后端,无需额外系统组件)
  3. 设 PPCLOCK_MCP_TOKEN;启动 ppclock-mcp --transport http --host 0.0.0.0 --port 8972 --mac AA:BB:…
  4. 防火墙放行 8972(仅局域网);不动既有服务与 8971 bridge
  5. 测试期手动启动即可;稳定后再议是否注册为 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 部署文档同步更新。
  • 2026-07-31 HTTP 复测(熠管家,全部命中):C:\ppclock 更新到 e8feb2d, --allowed-hosts "192.168.61.35:8972,localhost:8972,127.0.0.1:8972" 启动; 从 LAN 主机 192.168.61.56 实测:无 token → 401;Host=evil.example → 421; 真实 LAN Host → 200(serverInfo.name=ppclock);device_status 全链 ok=true、connected=true、MAC=18:BC:5A:5D:BF:28。8971 与既有服务未动, 8972 测后已停止(未注册服务、token 未持久化——常驻运行方案留后续迭代)。