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

154 lines
8.1 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 未持久化——常驻运行方案留后续迭代)。