# 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 `,不符即 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": , "data": {...}}`; 失败 `{"ok": false, "tool": , "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 部署文档同步更新。