From 3b62195a36e2f10cb056bd408118721f664115bd Mon Sep 17 00:00:00 2001 From: Jerry Yan <792602257@qq.com> Date: Tue, 21 Jul 2026 13:18:10 +0800 Subject: [PATCH] docs(superpowers): add host-agent MCP server design spec Design for mounting a Streamable HTTP MCP server inside the host-agent process so Hermes Agent (or any MCP client) can drive devices directly. Reuses the existing console FastAPI + uvicorn on port 8765, adds bearer- token auth, per-device session-level locks with 60s TTL, and cloud coordination via a new heartbeat field. Cloud scheduler skips devices reported as MCP-busy. Co-Authored-By: Claude Opus 4.6 --- ...2026-07-21-host-agent-mcp-server-design.md | 432 ++++++++++++++++++ 1 file changed, 432 insertions(+) create mode 100644 docs/superpowers/specs/2026-07-21-host-agent-mcp-server-design.md diff --git a/docs/superpowers/specs/2026-07-21-host-agent-mcp-server-design.md b/docs/superpowers/specs/2026-07-21-host-agent-mcp-server-design.md new file mode 100644 index 0000000..fbe9065 --- /dev/null +++ b/docs/superpowers/specs/2026-07-21-host-agent-mcp-server-design.md @@ -0,0 +1,432 @@ +# Host-Agent MCP Server — Design Spec + +- **Date**: 2026-07-21 +- **Status**: Draft, pending user review +- **Owner**: Jerry Yan +- **Target package**: `apps/device-host-agent`(主)+ `packages/cloud-platform`(schema/scheduler 扩展)+ `api/mcp.py`(一处签名收紧) + +## 1. Overview + +在 host-agent 进程内挂载 Streamable HTTP MCP server,让 Hermes Agent(或任意 MCP 客户端)作为外部大脑直接驱动 host-agent 管理的设备,与既有的 Cloud Control Plane 长轮询 worker 路径并存。 + +**一句话**:Hermes 当大脑,host-agent 当手;同一个 host-agent 进程同时服务两条职责,per-device 互斥,先到先得。 + +## 2. Background & Motivation + +- 当前 host-agent 只能通过 Cloud Control Plane 派发任务驱动;操作员想直接用本地 Hermes Agent 临时操控设备时,必须先在 cloud 侧创建 task,路径长、延迟高、依赖网络。 +- `api/mcp.py::create_mcp_server()` 已经用 FastMCP 暴露了 11 个设备工具(`take_screenshot`/`tap`/`swipe`/`input_text`/`launch_app`/`find_text`/`find_icon`/`get_ui_tree`/`describe_screen`/`list_devices`/`device_status`),但只服务于 Runtime 进程(port 8000),且没有可运行入口被 host-agent 复用。 +- Hermes Agent 官方支持 HTTP MCP client(见 [Hermes MCP docs](https://hermes-agent.nousresearch.com/docs/user-guide/features/mcp)),配置长这样: + ```yaml + mcp_servers: + apex_device: + url: "http://127.0.0.1:8765/mcp" + headers: + Authorization: "Bearer " + ``` +- uv.lock 已锁定 `mcp==1.28.1`,支持 Streamable HTTP transport。 +- 既有的 `host-agent-local-console` 已立先例:host-agent 进程内 always-on 跑 FastAPI + uvicorn(默认 port 8765,loopback only),本次 MCP server 沿用同一 server、同一端口,仅新增一个 `/mcp` mount。 + +## 3. Locked Decisions(来自 grill 阶段) + +| # | 决策 | 备注 | +|---|---|---| +| D1 | Transport = Streamable HTTP | mcp SDK 1.28.1 支持 | +| D2 | Mount 路径 = `/mcp`,与 console 同端口(默认 8765) | FastAPI `app.mount()` | +| D3 | 网络 = loopback only,沿用 `HOST_AGENT_CONSOLE_ALLOW_NON_LOOPBACK` 双保险 | 不新增 bind 配置 | +| D4 | 鉴权 = 独立 MCP bearer token,存 `host_mcp_token.json` | 文件路径 = `config.identity_path.parent / "host_mcp_token.json"` | +| D5 | 并发锁 = per-device,先到先得 + fail-fast | 不做 wait 队列 | +| D6 | 设备状态映射 = 走 `_device_display_status()` 同款逻辑 | 避免"所有连上的设备看起来都 busy" | +| D7 | Cloud worker 与 MCP server 在同一 host-agent 进程并存 | 不互斥,共享 `DeviceManager` | +| D8 | Cloud ↔ MCP 协调 = 心跳上报 `mcp_busy_device_ids`,cloud scheduler 跳过 | 心跳 schema 扩展,cloud 侧 _matches() 一处改动 | +| D9 | MCP session 级 lazy acquire 锁,60s TTL 兜底 | session_id 来自 FastMCP 上下文 | +| D10 | Skill catalog 工具 MVP 不暴露,保留 `create_mcp_server(skill_catalog_store=...)` 参数化挂载点 | 未来可加 mutating 工具 | +| D11 | `wait_until_usable` 方法实现 + 单测,但调用方不接入 | 预留能力,MVP 全部 fail-fast | +| D12 | `tool_handlers(manager)` 改为必传 | 已 grep 确认无调用方依赖 None 默认,根治 `DeviceNotFoundError` 类静默回退地雷 | + +## 4. Architecture + +### 4.1 总览 + +``` +┌──────────────────────────── host-agent 进程 ────────────────────────────┐ +│ │ +│ run_async() 主循环 │ +│ ├─ HeartbeatSynchronizer ──reads──→ McpBusyTracker ──┐ │ +│ │ (已有) payload.mcp_busy_device_ids │ │ +│ ├─ AssignmentProcessor ──reads──→ AgentStatusTracker │ │ +│ │ (已有,cloud 路径) .current_assignment │ +│ │ └─ AssignmentExecutor.execute() 启动前 fail-fast │ │ +│ │ 检查 mcp_busy_tracker.busy_device_ids() │ │ +│ └─ Console uvicorn server(已有 port 8765) │ │ +│ └─ FastAPI app │ │ +│ ├─ /login, /devices, /tasks, ... (已有) │ │ +│ └─ Mount("/mcp") │ │ +│ └─ FastMCP.streamable_http_app() │ │ +│ ├─ BearerAuthMiddleware ──verify──→ McpTokenStore │ +│ └─ 11 tools(包装层) │ │ +│ ├─ busy check ────────────────────┘ │ +│ │ (cloud 占用? McpBusyTracker 占用?) │ +│ ├─ acquire device lock(首次调用时) │ +│ ├─ tool_handlers(manager=)[name]│ +│ └─ release on session end / TTL │ +│ │ +└─────────────────────────────────────────────────────────────────────────┘ + ▲ ▲ + │ HTTP + Bearer │ HTTP heartbeat + │ │ + mcp_busy_device_ids +┌───────────┴───────────┐ ┌─────────┴─────────┐ +│ Hermes Agent (client) │ │ Cloud Control │ +│ ~/.hermes/config.yaml │ │ Plane (server) │ +│ mcp_servers.apex.url │ │ scheduler skips │ +│ = http://127.0.0.1 │ │ mcp-busy devices │ +│ :8765/mcp │ │ │ +└────────────────────────┘ └────────────────────┘ +``` + +### 4.2 关键不变量 + +- `DeviceManager` 单例;cloud 路径和 MCP 路径都通过显式参数注入。包装层在 `manager is None` 时抛错而非回退 `DEFAULT_MANAGER`(D12)。 +- 同一设备同一时刻只能被一边占用:cloud 占用 → MCP busy 错误;MCP 占用 → 心跳上报 → cloud scheduler 跳过;窗口期内冲突由 AssignmentExecutor 启动前 fail-fast 兜底。 +- `runtime/` / `api/` 包边界不破坏:所有新代码在 `host_agent/`;`api/mcp.py` 只做一处签名收紧。 + +## 5. Components + +### 5.1 `host_agent/mcp_lock.py`(新)— `McpBusyTracker` + +```python +@dataclass(frozen=True) +class McpDeviceLease: + device_id: str + session_id: str + acquired_at: datetime + last_seen_at: datetime + +class McpBusyTracker: + def __init__(self, *, ttl_seconds: float = 60.0, now=None) -> None: ... + def acquire(self, device_id: str, session_id: str) -> bool: ... + def renew(self, device_id: str, session_id: str) -> bool: ... + def release(self, session_id: str) -> list[str]: ... + def release_device(self, device_id: str, session_id: str) -> bool: ... + def busy_device_ids(self) -> list[str]: ... # lazy sweep + def snapshot(self) -> list[McpDeviceLease]: ... # lazy sweep, for UI + def wait_until_usable( + self, device_id: str, session_id: str, *, + timeout: float, poll_interval: float = 1.0, + cloud_busy_check: Callable[[], bool] | None = None, + ) -> bool: ... # 预留,MVP 不被调用 +``` + +- 进程内单实例,`threading.Lock` 保护。 +- TTL sweep 在 `busy_device_ids()` / `snapshot()` 调用时 lazy 执行。 +- Cloud 占用检查**不放这里**——保持单一职责;调用方在 acquire 前查 `AgentStatusTracker`。 + +### 5.2 `host_agent/mcp_token.py`(新)— `McpTokenStore` + +```python +@dataclass(frozen=True) +class McpToken: + version: int # =1 + token: str # secrets.token_urlsafe(32) + created_at: datetime + +class McpTokenStore: + def __init__(self, path: Path, *, now=None) -> None: ... + def load_or_create(self) -> McpToken: ... # atomic, 0o600 + def verify(self, presented: str) -> bool: ... +``` + +- 文件路径:`config.identity_path.parent / "host_mcp_token.json"`。 +- JSON 格式:`{"version": 1, "token": "...", "created_at": "2026-07-21T..."}`。 +- 写文件用 `tempfile + os.replace` 原子重命名;权限 0o600。 +- 文件损坏或不可写 → `McpTokenStoreError`,不静默重新生成。 + +### 5.3 `host_agent/web/mcp_auth.py`(新)— Bearer Auth Middleware + +```python +class BearerAuthMiddleware(BaseHTTPMiddleware): + def __init__(self, app, token_store: McpTokenStore) -> None: ... + # 401 + WWW-Authenticate: Bearer + JSON {"error":"invalid token"} on fail +``` + +- 挂在 FastMCP `streamable_http_app()` 外层(不是 console FastAPI 外层)。 +- 不写失败日志(避免暴力枚举刷屏);首次成功鉴权写 INFO。 + +### 5.4 `host_agent/web/mcp.py`(新)— `build_mcp_server()` + +```python +def build_mcp_server( + *, + manager: DeviceManager, + mcp_busy_tracker: McpBusyTracker, + status_tracker: AgentStatusTracker, +) -> FastMCP: + """Wraps api.mcp.tool_handlers(manager=manager) with: + - busy check (cloud OR mcp-busy → JSON-RPC -32000) + - lazy device lock acquire on first call + - lease renew on every call + - status mapping for list_devices / device_status (display_status)""" +``` + +- 单一 decorator 包装所有 11 个工具,避免重复。 +- `session_id` 来自 FastMCP streamable HTTP 上下文。 +- `list_devices` / `device_status` 不走 busy 检查(无 device_id 参数),但走 display status 映射。 + +### 5.5 改动的现有模块 + +| 模块 | 改动 | +|---|---| +| `host_agent/app.py::create_application()` | 构造 `McpTokenStore` / `McpBusyTracker` / `build_mcp_server()`,传给 `create_console_app()`;InstanceLock acquire 后、heartbeat 启动前完成 token 生成 | +| `host_agent/web/app.py::create_console_app()` | 新参数 `mcp_server` / `mcp_token_store` / `mcp_busy_tracker`;`app.mount("/mcp", auth_wrapped(server.streamable_http_app()))`;`/api/status` 加 `mcp_busy_devices` 字段 | +| `host_agent/web/templates/dashboard.html` | 一行 MCP 状态显示 | +| `host_agent/cli.py` | 新子命令 `mcp-token` 打印当前 token(不存在则生成) | +| `host_agent/heartbeat.py` | payload 加 `mcp_busy_device_ids` 字段,值来自 `mcp_busy_tracker.busy_device_ids()` | +| `host_agent/assignment.py::AssignmentExecutor` | 构造参数加 `mcp_busy_tracker: McpBusyTracker \| None = None`(None 时跳过检查,保持向后兼容;host-agent 装配时必传);`execute()` 启动前(`_execute_goal` / `_execute_workflow` 实际跑之前)fail-fast 检查 `mcp_busy_tracker.busy_device_ids()`,命中则返回 `status="failed"` + `failure_reason="device held by active MCP session"` | +| `cloud/internal_api/models.py` | Heartbeat payload schema 加 `mcp_busy_device_ids: list[str] = []` | +| `cloud/scheduler.py::_matches()` | 心跳里的 `mcp_busy_device_ids` 内的 device 视为非 idle | +| `api/mcp.py::tool_handlers` | 签名改为 `tool_handlers(*, manager: DeviceManager)`(必传,D12 根治) | + +## 6. Data Flows + +### 6.1 启动 + +``` +create_application() + ├─ InstanceLock acquire(已有) + ├─ resolve_host_identity()(已有) + ├─ DeviceManager 构造 + 设备注册(已有) + ├─ McpTokenStore(identity_path.parent / "host_mcp_token.json").load_or_create() + │ └─ 首次:生成 token、atomic 写文件、INFO 日志 "MCP token generated at " + ├─ McpBusyTracker(ttl_seconds=60) + ├─ build_mcp_server(manager=manager, mcp_busy_tracker=..., status_tracker=...) + └─ create_console_app(..., mcp_server=server, mcp_token_store=..., mcp_busy_tracker=...) + └─ app.mount("/mcp", auth_wrapped(server.streamable_http_app())) + +run_async() 主循环启动(行为不变): + ├─ heartbeat_task: 每 30s 读 mcp_busy_tracker.busy_device_ids() → payload + ├─ claim loop: 不变(仍会 claim cloud 任务) + └─ console_server: 现在同时服务 / * (HTML) 和 /mcp (Streamable HTTP) +``` + +### 6.2 Hermes 第一次 MCP 调用(lazy acquire) + +``` +1. Hermes → POST /mcp (JSON-RPC tools/call, name=take_screenshot, args={device_id: "phone-1"}) +2. BearerAuthMiddleware: verify token → pass +3. FastMCP route → 包装层 decorator: + a. session_id = ctx.session_id + b. busy check: + - cloud 占用? → status_tracker.snapshot()["current_assignment"]["device_id"] == "phone-1"? NO + - mcp 占用? → mcp_busy_tracker.busy_device_ids() 包含 "phone-1"? NO + c. acquire: mcp_busy_tracker.acquire("phone-1", session_id) → True + d. tool_handlers(manager=...)["take_screenshot"](device_id="phone-1") → screenshot + e. renew: mcp_busy_tracker.renew("phone-1", session_id) + f. return screenshot_base64 +4. 下一次心跳(≤30s): payload.mcp_busy_device_ids = ["phone-1"] +5. Cloud scheduler 收到 → 把 phone-1 视为非 idle → 不派任务给它 +``` + +### 6.3 同 session 第二次调用(锁已持有) + +``` +1. Hermes → POST /mcp (tap, device_id: "phone-1") +2-3a. 同上 +3b. mcp_busy_tracker.busy_device_ids() 包含 "phone-1",但持有者就是当前 session_id → 通过 +3c. acquire 已持有,no-op(或 assert) +3d-f. 同上 +``` + +### 6.4 Cloud 任务抢占的预防 + +``` +场景:Hermes 正在用 phone-1,cloud 这时派任务给 phone-1 + +正常路径: +1. Cloud scheduler 选设备:phone-1 在最近心跳的 mcp_busy_device_ids 内 → 视为非 idle → 跳过 +2. Cloud 选 phone-2 或等其他设备 idle + +窗口期兜底(心跳 30s 内 cloud 基于旧心跳派任务): +1. AssignmentProcessor.process(assignment) → AssignmentExecutor.execute(assignment) +2. execute() 启动前检查: + if assignment.device_id in mcp_busy_tracker.busy_device_ids(): + return AssignmentExecutionResult( + status="failed", + failure_reason="device held by active MCP session" + ) +3. client.report_result(...) 上报 fail → cloud attempt+1 或派给别的 host +``` + +### 6.5 Session 结束 / TTL 过期 + +``` +正常:Hermes 主动断开 → FastMCP 触发 session shutdown callback + → mcp_busy_tracker.release(session_id) → 该 session 持有的所有 lease 释放 + → 下次心跳 payload 不再包含这些 device_id → cloud 重新视为 idle + +异常:Hermes 崩溃 / 网络断 → 无 shutdown callback + → 60s TTL 到期 → 下次 busy_device_ids() 或 snapshot() 调用时 lazy sweep + → lease 清理 → 下次心跳 payload 不再包含 → cloud 重新视为 idle +``` + +## 7. Error Handling Matrix + +| # | 触发条件 | 返回语义 | 备注 | +|---|---|---|---| +| 1 | `Authorization` 缺失/不匹配 | HTTP 401 + `WWW-Authenticate: Bearer` + JSON `{"error":"invalid token"}` | 不写失败日志;首次成功鉴权写 INFO | +| 2 | Cloud 占用目标设备 | JSON-RPC `-32000` + `"device X is busy (held by cloud assignment)"` + `data.busy_owner = "cloud_assignment"` | DEBUG 日志 | +| 3 | 另一 MCP session 占用 | JSON-RPC `-32000` + `"device X is busy (held by another MCP session)"` + `data.busy_owner = "mcp_session:"` | DEBUG 日志 | +| 4 | `device_id` 不存在 | JSON-RPC `-32602` + `"unknown device: X"` | 复用 `call_with_semantic_errors` | +| 5 | 工具底层异常 | JSON-RPC `-32000` + 原异常 message | WARNING + exc_info | +| 6 | Cloud assignment 启动前命中 MCP 占用 | `AssignmentExecutionResult(status="failed", failure_reason="device held by active MCP session")` | INFO 一次 | +| 7 | Token 文件损坏 JSON | host-agent 启动失败,stderr 提示 | 不静默重新生成 | +| 8 | Token 文件不可写 | host-agent 启动失败 | 同上 | +| 9 | MCP session 异常断开 | lease 进入 TTL 倒计时 | 60s 后 lazy sweep | +| 10 | TTL 过期瞬间 Hermes 重连 | renew 容忍边界:session_id 匹配 → 重新 acquire 而非报错 | 无感 | +| 11 | 同 session 并发不同设备 | 各自独立 acquire | per-device 设计 | +| 12 | 同 session 并发同一设备 | 第一个 acquire;第二个 renew(同 session_id) | 并发 safe | +| 13 | FastMCP 提取不到 session_id | JSON-RPC `-32001` + `"cannot determine MCP session"` | ERROR + exc_info | +| 14 | host-agent 关停时有 active MCP session | lease 随进程退出消失 | 不需显式清理 | + +### 错误返回格式 + +```json +{ + "jsonrpc": "2.0", + "id": "", + "error": { + "code": -32000, + "message": "device phone-1 is busy (held by cloud assignment)", + "data": { + "device_id": "phone-1", + "busy_owner": "cloud_assignment" + } + } +} +``` + +## 8. Testing Strategy + +### 8.1 单元测试 + +- **`McpBusyTracker`**(~10 用例):acquire/renew/release/busy_device_ids/snapshot/wait_until_usable 的成功/冲突/并发/TTL sweep +- **`McpTokenStore`**(~6 用例):首次创建/二次复用/并发 atomic/verify timing-safe/损坏 JSON 抛错/不可写抛错 +- **`BearerAuthMiddleware`**(~5 用例):无 header/错误 token/正确 token/非 Bearer scheme/大小写 +- **包装层 decorator**(~10 用例,**重点**): + - 显式 `manager=` 传入 → 工具调用成功(**回归 D12 地雷**) + - `manager=None` → 包装层断言失败(**新约束**) + - cloud 占用 → `-32000` busy(mock AgentStatusTracker) + - 另一 MCP session 占用 → `-32000` busy + - 同 session 已持有 → renew 后通过 + - acquire/renew 调用顺序 + - 底层 `DeviceNotFoundError` → `-32602` + - 底层 `DriverError` → `-32000` + - `list_devices` 不 busy check 但走 display status + - `device_status` 同上 + +### 8.2 集成测试 + +- **Mount wiring**(~4 用例):`/mcp` mount 存在、不继承 cookie session、`/api/status` 加字段、dashboard HTML 含状态行 +- **心跳 payload**(~3 用例):空 / 含 phone-1 / TTL 过期后空 +- **AssignmentExecutor fail-fast**(~2 用例):命中 MCP 占用立即 fail、不命中正常执行 +- **Cloud 侧**(~4 用例):Heartbeat model 向后兼容空 list、含 device、`_matches()` 跳过 mcp_busy、旧 host-agent 不发字段时行为不变 + +### 8.3 端到端(1 个,可选) + +- MCP 客户端模拟(`mcp` SDK client + ASGI TestClient)→ host-agent 进程内 → FakeDriver +- 覆盖:鉴权 → list_devices → screenshot → tap → 中途 cloud assignment fail-fast → session 断开 release +- 如果 SDK client + TestClient 组合有坑,降级为直接调用 FastMCP tool registry + +### 8.4 已有测试不破坏 + +- `test_app.py` 9 处 `create_application()` 调用:新组件在 `create_application()` 内部构造,外部 API 不变 +- `test_runtime_owned_packages_do_not_import_host_or_cloud_concerns`:所有新代码在 `host_agent/`,不动 `runtime/` / `api/` 包边界(`api/mcp.py` 签名收紧不算破坏) +- 根非集成测试套件:跑一遍无回归 + +### 8.5 人工/真机(不在自动化覆盖) + +- 真实 Hermes Agent CLI 接入(写 SOUL.md、跑真实对话) +- 真机 iPhone/Android MCP 工具调用 + +## 9. Scope + +### 9.1 In Scope + +见第 5 节 Components 表。共: +- 4 个新模块(`mcp_lock.py` / `mcp_token.py` / `web/mcp_auth.py` / `web/mcp.py`) +- 8 个改动的现有模块 +- 2 个文档(`docs/MCP_INTEGRATION.md` 新增 + `docs/MACOS_IPHONE_SETUP.md` 加小节) +- 单元 + 集成测试全覆盖;e2e 视成本而定 + +### 9.2 Non-Goals + +- `wait_until_usable` 的调用方接入(方法实现 + 单测,但 AssignmentExecutor 和 MCP 工具包装层都不调用) +- 暴露 `wait_until_device_usable` 成独立 MCP 工具 +- `device-host-agent mcp-rotate-token` CLI(MVP 轮换走"删文件 + 重启") +- MCP 调用进 `ConsoleHistoryStore`(MVP 不记录调用历史) +- Skill catalog 工具暴露给 Hermes(保留参数化挂载点) +- Token rate limiting / 失败计数 +- 多 Hermes 实例协调(技术允许但不专门测试) +- 远程访问(非 loopback)支持 +- Hermes 侧 SOUL.md / 配置自动化 +- MCP over WebSocket +- Cloud scheduler 改造为支持"MCP 优先级"/队列 + +## 10. Open Questions + +- **Q1(已答)**:`tool_handlers(manager)` 是否改为必传?→ **是**。Grep 全仓库确认所有调用方(`api/mcp.py:110`、`tests/test_mcp.py:13,56`、`tests/test_skill_catalog_e2e.py:287`)都已显式传 `manager=manager`,改动面为 0。 +- **Q2**:`mcp-token` CLI 是否需要鉴权?→ MVP 不鉴权,假定能访问宿主机的操作者可信(与 `setup` 子命令同款)。后续可加 `--password` 校验 local_account。 +- **Q3**:MCP 工具调用是否需要 wall-clock 超时?→ MVP 不加,依赖底层超时;如有"Hermes 调用挂死"报告再加。 +- **Q4**:心跳扩展是 Alembic migration 还是仅 schema 字段?→ MVP 仅 transient 字段(heartbeat 接收 → scheduler 用 → 丢弃),无需 migration。 +- **Q5(待实测)**:60s TTL 是否合适?→ 实测后调整。太短容易误释放正常 session 的锁;太长 Hermes 崩溃后设备不可用窗口大。 + +## 11. Risks + +- **R1**:Cloud 心跳窗口期(30s)冲突。缓解:AssignmentExecutor 启动前 fail-fast。残留:cloud 可能基于过期心跳派任务、host-agent fail、cloud 重试——浪费 attempt 配额。**接受**。 +- **R2**:Hermes 长时间占用设备导致 cloud 任务反复 fail。MVP 无自动缓解;用户手动管控;未来启用 `wait_until_usable`。**接受**。 +- **R3**:Hermes 崩溃后 60s 内设备不可用。TTL 兜底但窗口存在。**接受,记入 Q5**。 +- **R4(已消除)**:`tool_handlers` 签名改动影响面。Grep 确认 0 调用方依赖 None 默认。 +- **R5**:FastMCP `session_id` 提取依赖 `mcp` SDK 内部 API。缓解:e2e 测试覆盖;SDK 升级 CI 能及时暴露。 +- **R6**:`mcp` SDK 需作为 device-host-agent 直接依赖(目前通过 Runtime 传递)。需加进 `apps/device-host-agent/pyproject.toml`(与 `filelock` 直接化先例一致)。 +- **R7**:Token 文件首次生成的竞态。缓解:InstanceLock 前置保护;`tempfile + os.replace` 原子重命名。 + +## 12. Coordination with Existing Changes + +- **`host-agent-single-instance-lock`**:本次依赖 InstanceLock,token 文件生成在 InstanceLock acquire 之后,安全。无需修改。 +- **`host-agent-dependency-supervisor`**:本次不引入新的外部进程依赖(FastMCP 是库)。不冲突。 +- **`task-execution-progress-visibility`**:本次不改 `TaskMetadataStore` / `Timeline`。不冲突。 +- **`host-agent-local-console`**:本次复用其 FastAPI + uvicorn 设施,新增一个 mount 点。Console 现有 cookie session 鉴权**不**继承到 `/mcp`(鉴权走独立 bearer middleware)。 + +## 13. Hermes 侧配置示例(参考,非 host-agent 代码范围) + +`~/.hermes/config.yaml`: +```yaml +mcp_servers: + apex_device: + url: "http://127.0.0.1:8765/mcp" + headers: + Authorization: "Bearer " +``` + +首次启动 host-agent 后,从 `tasks/host_mcp_token.json`(或 `HOST_AGENT_IDENTITY_PATH` 同目录)读 token,或跑 `device-host-agent mcp-token` 打印。 + +Hermes 的 `SOUL.md`(profile 级别)建议补充: +- 工具调用前先 `list_devices` 看可用设备 +- 设备 `busy` 时等待或换设备 +- iOS 设备坐标是 points(非 pixels),Android 是 pixels +- OCR/UI 树 bounds 与 screenshot 像素已对齐(perception 层已处理) + +## 14. Implementation Order(建议,writing-plans 阶段细化) + +1. `api/mcp.py::tool_handlers` 签名收紧(D12)+ 同步更新调用方 docstring/类型 +2. `McpTokenStore` + 单测 +3. `McpBusyTracker`(含 `wait_until_usable`)+ 单测 +4. `BearerAuthMiddleware` + 单测 +5. `build_mcp_server` 包装层 + 单测(覆盖 D12 回归) +6. `create_console_app` mount wiring + 集成测试 +7. `create_application` 装配 +8. 心跳 payload 扩展 + cloud scheduler 改动 + 集成测试 +9. `AssignmentExecutor` fail-fast 检查 + 测试 +10. CLI `mcp-token` 子命令 +11. Dashboard 状态行 +12. 文档(`MCP_INTEGRATION.md` + `MACOS_IPHONE_SETUP.md`) +13. e2e 测试(可选) +14. 全量非集成测试回归 + ruff + compileall + openspec strict validation