From de683fe9c297c217c76490ca2770137114cdbdf4 Mon Sep 17 00:00:00 2001 From: chenwei Date: Thu, 30 Jul 2026 16:59:07 +0000 Subject: [PATCH] =?UTF-8?q?docs+release:=20MCP.md=20=E5=B7=A5=E5=85=B7?= =?UTF-8?q?=E5=8F=82=E8=80=83=E4=B8=8E=E9=83=A8=E7=BD=B2=E6=8C=87=E5=8D=97?= =?UTF-8?q?=EF=BC=9B0.2.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude --- CHANGELOG.md | 19 ++++++ README.md | 20 +++++- docs/sdk/API.md | 7 +- docs/sdk/MCP.md | 138 ++++++++++++++++++++++++++++++++++++++++ llms.txt | 15 +++-- pyproject.toml | 2 +- src/ppclock/__init__.py | 3 +- 7 files changed, 193 insertions(+), 11 deletions(-) create mode 100644 docs/sdk/MCP.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 27bdaaf..e80c205 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,24 @@ # CHANGELOG +## [0.2.0] — 2026-07-30 + +新增 **ppclock-mcp**:跑在蓝牙物理所在机器上的单层 MCP 服务器,agent 经 stdio 或 +streamable HTTP 直驱墨水屏。 + +### MCP 服务器 +- `ppclock-mcp` 命令(`ppclock.mcp_server:main`):stdio / streamable HTTP 双模, + HTTP 绑非回环地址强制 `PPCLOCK_MCP_TOKEN` Bearer 鉴权(ASGI 中间件,不符 401) +- 13 个工具(`ppclock.mcp_tools`):scan_devices / device_status / set_time / set_mode / + toggle / upload_image(路径或 base64,6 抖动+6 调整参数)/ render_template(6 模板)/ + countdown / countdown_off / calendar_text / set_sleep / clear_screen / raw_send +- 统一输出契约与 CLI `--json` 同构:`{"ok","tool","data"|"error":{"code","message","hint"?}}`, + 错误码 CONNECT_TIMEOUT / DEVICE_NOT_FOUND / BLE_ERROR / INVALID_PARAM / INTERNAL +- **DeviceManager 公开为子包能力**(`ppclock.device_manager`,方案 C 连接管理): + 按需连接 + 空闲保持,守候窗口覆盖设备 30-60s 广播窗口(`--connect-timeout` 默认 90s), + 空闲自动断开(`--idle-timeout` 默认 300s),全操作串行,掉线重连重试一次 +- 有意不暴露:OTA / 激活 / LUT / WiFi / 轮播(高危或本固件 no-op,走 SDK/CLI) +- 文档:`docs/sdk/MCP.md`(工具参考 + agent 挂载 + Windows 部署) + ## [0.1.0] — 2026-07-30 首个 SDK 化发布。4.2寸墨水屏设备(DA14585 BLE,400×300 黑白红三色)的**完全离线**开发包: diff --git a/README.md b/README.md index 2fcbec7..0a56e7d 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,24 @@ async with PPClient.via_bridge("192.168.61.35:8971") as dev: # 经 bridge 设备操作全集:对时(tz)/模式/切换/传图/6 模板/倒计时/休眠/停车牌/轮播/LUT/WiFi/raw/OTA, 见 [`docs/sdk/API.md`](docs/sdk/API.md)。 +## MCP 服务器(agent 直挂) + +`ppclock-mcp` 在蓝牙物理所在机器上跑单层 MCP 服务器,13 个工具覆盖对时/模式/传图/ +模板/倒计时/休眠等日常操作: + +```bash +pip install -e ".[mcp]" # 追加依赖 mcp + +# stdio —— Claude Code 等本机 agent 直挂 +claude mcp add ppclock -- ppclock-mcp --mac AA:BB:CC:DD:EE:FF + +# streamable HTTP —— 局域网 agent 平台(Bearer 鉴权,端点 /mcp) +PPCLOCK_MCP_TOKEN= ppclock-mcp --transport http --host 0.0.0.0 --port 8972 +``` + +连接管理为方案 C(按需连接+空闲保持、串行、掉线重连一次);OTA/激活/LUT/WiFi/轮播 +有意不暴露。工具参考与部署指南见 [`docs/sdk/MCP.md`](docs/sdk/MCP.md)。 + ## CLI 用法 ```bash @@ -97,7 +115,7 @@ ppclock --json ota firmware.img --yes # SUOTA ## 开发 ```bash -.venv/bin/python -m pytest tests/ -q # 165 测试 +.venv/bin/python -m pytest tests/ -q # 195 测试 ``` 治理规则见 `GOVERNANCE.md`;变更记录 `CHANGELOG.md`;决策日志 `DECISIONS.md`。 diff --git a/docs/sdk/API.md b/docs/sdk/API.md index 3b803c3..d8475be 100644 --- a/docs/sdk/API.md +++ b/docs/sdk/API.md @@ -1,6 +1,6 @@ -# ppclock SDK · API 参考(v0.1.0) +# ppclock SDK · API 参考(v0.2.0) -> 面向二次开发的签名级参考。协议细节见 `docs/protocol.md`,固件内部见 `docs/firmware-analysis.md`。 +> 面向二次开发的签名级参考。协议细节见 `docs/protocol.md`,固件内部见 `docs/firmware-analysis.md`。MCP 服务器(ppclock-mcp)工具参考见 `docs/sdk/MCP.md`。 ## 安装与入口 @@ -84,6 +84,9 @@ from ppclock import PPClient | `ppclock.firmware.image` | `parse_image/unpack_body/pack_image`('pQ' 镜像) | | `ppclock.firmware.ota` | `run_ota/package_image/xor_checksum/STATUS_ERRORS` | | `ppclock.bridge_server` | `run()`(`ppclock-bridge` 入口) | +| `ppclock.mcp_server` | `main()`(`ppclock-mcp` 入口,stdio/HTTP 双模)、`build_server`、`TokenAuthMiddleware` | +| `ppclock.device_manager` | `DeviceManager`(方案 C:按需连接+空闲保持、串行、重连重试一次) | +| `ppclock.mcp_tools` | `register_tools`(13 工具注册)、`decode_image_source` | ## Bridge RPC 协议(v1,JSONL/TCP,默认 8971) diff --git a/docs/sdk/MCP.md b/docs/sdk/MCP.md new file mode 100644 index 0000000..c1f7224 --- /dev/null +++ b/docs/sdk/MCP.md @@ -0,0 +1,138 @@ +# ppclock-mcp · MCP 服务器参考(v0.2.0) + +> 面向 agent 挂载与部署的工具级参考。SDK 签名见 `docs/sdk/API.md`,协议细节见 `docs/protocol.md`。 + +## 一句话 + +ppclock-mcp 是跑在**蓝牙物理所在机器**上的单层 MCP 服务器:13 个工具覆盖对时 / 模式 / +切换 / 传图 / 模板 / 倒计时 / 日历文字 / 休眠 / 刷屏 / raw 逃生舱,agent 经 stdio 或 +streamable HTTP 直接驱动 4.2" 墨水屏设备。 + +## 安装 + +```bash +pip install -e ".[mcp]" # 追加依赖:mcp>=1.10,<2 +``` + +要求:Python ≥ 3.10、本机蓝牙适配器(bleak)。服务器与被控设备在同一台蓝牙主机上; +不需要互联网。 + +## 启动 + +入口为 console script `ppclock-mcp`(`python -m ppclock.mcp_server` 不受支持)。 + +```bash +# stdio —— 本机 agent 直挂 +ppclock-mcp --mac AA:BB:CC:DD:EE:FF + +# streamable HTTP —— 局域网 agent 平台远程调用 +PPCLOCK_MCP_TOKEN= ppclock-mcp --transport http --host 0.0.0.0 --port 8972 +``` + +启动参数: + +| 参数 | 默认 | 说明 | +|---|---|---| +| `--transport` | `stdio` | `stdio` / `http` | +| `--host` | `127.0.0.1` | HTTP 绑定地址;绑非回环地址**必须**设 `PPCLOCK_MCP_TOKEN`,否则拒绝启动 | +| `--port` | `8972` | HTTP 端口 | +| `--mac` | 无 | 设备 MAC;缺省自动扫描 `NRF-` 前缀并记住实际地址 | +| `--connect-timeout` | `90.0` | 守候连接秒数(覆盖设备 30-60s 广播窗口) | +| `--idle-timeout` | `300.0` | 空闲断开秒数;`0` = 每次调用独立连接 | + +环境变量:`PPCLOCK_MCP_TOKEN` —— HTTP 模式 Bearer 鉴权令牌(仅 HTTP 使用;stdio 无鉴权, +信任本机进程)。 + +## Agent 挂载示例 + +```bash +# Claude Code(stdio) +claude mcp add ppclock -- ppclock-mcp --mac AA:BB:CC:DD:EE:FF +``` + +streamable HTTP 端点为 `http://:8972/mcp`,请求头携带 +`Authorization: Bearer `;token 不符返回 401。HTTP 为无状态模式 +(`stateless_http`),无需会话保持。 + +## 13 工具参考 + +全部工具 async,返回统一契约(见下节)。参数与行为逐字对齐 `src/ppclock/mcp_tools.py`。 + +| 工具 | 参数 | 说明 | +|---|---|---| +| `scan_devices` | `timeout: float = 5.0` | 扫描附近 NRF- 前缀墨水屏设备,返回 `[{name,address,rssi}]` | +| `device_status` | 无 | 查询连接状态/绑定 MAC/设备 ID/空闲秒数 | +| `set_time` | `tz: float = 8.0` | 对时;tz 为时区偏移(默认 8=北京时间) | +| `set_mode` | `mode: str` | 切换显示模式:`clock1-3`/`calendar1-3`/`image0-3`/`tricolor`/`mono` | +| `toggle` | `name: str` | 单字节切换:`invert`/`font`/`rotate180`/`hour_format`/`clock_color` | +| `upload_image` | `source: str, slot: int = 0, algo: str = "atkinson", mono: bool = False, threshold/diffusion/brightness/contrast/saturation/rotate: float\|None = None` | 传图到指定槽位。`source` = 服务器路径或 base64(可带 data URI 前缀);`algo` ∈ `none/floydsteinberg/atkinson/bayer/stucki/jarvis`;6 个可选 adjust 参数:`threshold`/`diffusion`/`brightness`/`contrast`/`saturation`/`rotate`(仅显式传入时生效) | +| `render_template` | `name: str, payload: dict\|None = None, slot: int = 0, algo: str = "atkinson"` | 本地渲染模板并上传:`schedule`/`businesscard`/`memo`/`course`/`qrcode`/`custom` | +| `countdown` | `date: str, mode: str = "clock", prefix: str\|None = None` | 倒计时;`date` 为 ISO `YYYY-MM-DD`;`mode` = `clock`/`calendar` | +| `countdown_off` | 无 | 关闭倒计时 | +| `calendar_text` | `text: str` | 设置日历中文文字并切到日历模式一 | +| `set_sleep` | `on: bool, start_h: int, end_h: int` | 设置休眠时段(0-23 时) | +| `clear_screen` | 无 | 刷屏(EPD 清屏) | +| `raw_send` | `data_hex: str, channel: str = "alt"` | 逃生舱:直发十六进制字节串;`channel` = `rxtx`/`epd`/`alt` | + +`device_status` 返回字段:`connected` / `mac` / `device_id`(尽力而为,失败省略)/ +`idle_seconds` / `connect_count` / `connect_timeout` / `idle_timeout`。 + +## 输出契约与错误码 + +与 CLI `--json` 同构的统一契约: + +```json +成功 {"ok": true, "tool": "", "data": {...}} +失败 {"ok": false, "tool": "", "error": {"code", "message", "hint"?}} +``` + +错误码(5 种): + +| code | 触发 | hint | +|---|---|---| +| `CONNECT_TIMEOUT` | 守候窗口内未连上设备 | 设备长睡眠、广播窗口 30-60s;调大 `--connect-timeout` 或稍后重试 | +| `DEVICE_NOT_FOUND` | 扫描未发现设备 | 未发现 NRF- 前缀设备;确认设备在位且未处于深睡 | +| `BLE_ERROR` | 其他传输层错误 | 无 hint | +| `INVALID_PARAM` | 参数校验失败(非法 base64/非图片/非法日期/非法 hex 等) | 无 hint,message 给出具体原因 | +| `INTERNAL` | 工具层兜底未分类异常 | 无 hint,message 为 `类型: 详情` | + +`hint` 仅对 `CONNECT_TIMEOUT` / `DEVICE_NOT_FOUND` 出现,语义是给 agent 的可执行恢复建议 +(调参重试 / 检查设备),不是给人看的错误描述。 + +## 连接行为(方案 C:按需连接 + 空闲保持) + +- **守候窗口**:首次操作发起连接,守候 `--connect-timeout` 秒(默认 90s,覆盖设备 + 30-60s 广播窗口);设备在深睡中也能等到其广播。 +- **空闲超时**:操作完成后保持连接;空闲 `--idle-timeout` 秒(默认 300s)自动断开让 + 设备回睡眠。`--idle-timeout 0` 退化为每次调用独立连接。 +- **串行**:所有设备操作经一把锁串行执行(BLE 适配器独占),并发调用自动排队,不会 + 互相打断。 +- **重连一次**:操作中连接丢失(TransportError)→ 断开重连并重试一次,再失败才向上 + 返回错误。 + +## Windows 部署(蓝牙所在机) + +```powershell +python -m venv .venv +.venv\Scripts\pip install -e ".[mcp]" +setx PPCLOCK_MCP_TOKEN "" # 或按服务方式注入环境变量 +.venv\Scripts\ppclock-mcp --transport http --host 0.0.0.0 --port 8972 +``` + +- 防火墙放行 **8972/tcp**,仅对局域网段开放(勿暴露公网;token 是唯一防线)。 +- 不动既有 **8971** bridge 与其他服务:8972 为独立端口、独立进程。 +- `--mac` 建议显式指定设备地址,避免自动扫描误绑其他 `NRF-` 设备。 + +## 不暴露的能力 + +以下 SDK 能力**有意不**注册为 MCP 工具: + +| 能力 | 原因 | +|---|---| +| OTA 固件升级(`ota`) | 高危写闪存,agent 误调用可砖机;只走 CLI 显式 `--yes` 授权路径 | +| 激活(`activate`/`activate_auto`) | 一次性出厂级操作,日常 agent 场景不需要;保留在 SDK/CLI | +| LUT 校准(`lut`) | 面板厂商级调试参数,误刷影响显示质量 | +| WiFi 配置(`wifi`) | 本固件为 no-op(固件证据),暴露只会误导 agent | +| 轮播(`rotation`) | 本固件为 no-op(固件证据),同上 | + +需要这些能力时按 `docs/sdk/API.md` 走 SDK/CLI,不经 MCP 面。 diff --git a/llms.txt b/llms.txt index f266d20..19c002e 100644 --- a/llms.txt +++ b/llms.txt @@ -1,6 +1,6 @@ # ppclock — 4.2寸墨水屏设备(DA14585 BLE)离线 SDK -> AI 消费向项目卡(llms.txt v0.1.0)。人用文档见 README.md;签名级 API 见 docs/sdk/API.md。 +> AI 消费向项目卡(llms.txt v0.2.0)。人用文档见 README.md;签名级 API 见 docs/sdk/API.md;MCP 服务器见 docs/sdk/MCP.md。 ## 一句话 @@ -10,12 +10,13 @@ ppclock 是 4.2" 400×300 黑白红三色墨水屏设备(Dialog DA14585,广 ```bash python3 -m venv .venv && .venv/bin/pip install -e ".[dev]" -.venv/bin/python -m pytest tests/ -q # 165 全绿 +.venv/bin/python -m pytest tests/ -q # 195 全绿 ``` - 库:`from ppclock import PPClient` - CLI:`ppclock [--json] [--mac X] [--via bridge://host[:port]] ` - BLE 桥:`ppclock-bridge --host 0.0.0.0 --port 8971`(在他机蓝牙物理所在机运行,本机经 `--via` 透明使用) +- MCP 服务器:`ppclock-mcp [--mac X] [--transport http --host 0.0.0.0 --port 8972]`(`PPCLOCK_MCP_TOKEN` 鉴权;extra `.[mcp]`) ## 快速示例 @@ -28,6 +29,8 @@ async with PPClient.via_bridge("192.168.61.35:8971", mac="18:BC:5A:5D:BF:28") as await dev.activate_auto() # 离线激活(破解厂商云) ``` +MCP 挂载(agent 直挂 13 工具):`claude mcp add ppclock -- ppclock-mcp --mac X`,或 HTTP 端点 `http://:8972/mcp` + Bearer token;详见 docs/sdk/MCP.md。 + ## 架构(关键不变量) 1. **命令走 331f(alt 通道)**:1f1f 只 ATT-ACK 不执行(实机证据,勿用)。 @@ -38,11 +41,11 @@ async with PPClient.via_bridge("192.168.61.35:8971", mac="18:BC:5A:5D:BF:28") as ## 目录 -- `src/ppclock/`:client.py(门面)/ protocol / image_pipeline / templates / transports(base,local,bridge) / firmware(image,ota) / cli / bridge_server +- `src/ppclock/`:client.py(门面)/ protocol / image_pipeline / templates / transports(base,local,bridge) / firmware(image,ota) / cli / bridge_server / ppclock-mcp(mcp_server 入口 / device_manager 连接管理 / mcp_tools 13 工具) - `tools/`:fw_info/fw_pack(镜像工具)、field_test(真机引导测试)、flash_run、gpio_probe、rppclock.sh(守候重试) -- `docs/`:protocol.md(协议规范·唯一事实源)、sdk/API.md、architecture.md、firmware-analysis.md、firmware-layout.md、field-test-result.md +- `docs/`:protocol.md(协议规范·唯一事实源)、sdk/API.md、sdk/MCP.md、architecture.md、firmware-analysis.md、firmware-layout.md、field-test-result.md - `analysis/`:三线逆向证据(web/apk/firmware) -- `tests/`:165 测试 +- `tests/`:195 测试 ## 设备事实(实测 NRF-5DBF28) @@ -60,4 +63,4 @@ async with PPClient.via_bridge("192.168.61.35:8971", mac="18:BC:5A:5D:BF:28") as ## 版本 -0.1.0(CHANGELOG.md)。治理:GOVERNANCE.md(证据纪律/门禁);历史决策:DECISIONS.md。 +0.2.0(CHANGELOG.md)。治理:GOVERNANCE.md(证据纪律/门禁);历史决策:DECISIONS.md。 diff --git a/pyproject.toml b/pyproject.toml index 1de320a..81381c8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "ppclock" -version = "0.1.0" +version = "0.2.0" description = "4.2寸墨水屏设备离线 CLI 上位机(DA14585 BLE,agent 友好)" requires-python = ">=3.10" dependencies = [ diff --git a/src/ppclock/__init__.py b/src/ppclock/__init__.py index 8809447..efaffc0 100644 --- a/src/ppclock/__init__.py +++ b/src/ppclock/__init__.py @@ -14,8 +14,9 @@ - transports:local(bleak)/bridge(RPC) 传输实现 - firmware:镜像解析重打包(image)与 SUOTA(ota) - bridge_server:BLE 桥服务端(ppclock-bridge 命令) +- device_manager / mcp_tools / mcp_server:MCP 服务器(ppclock-mcp 命令) """ -__version__ = "0.1.0" +__version__ = "0.2.0" from .client import PPClient from .transports.base import Transport, TransportError