# 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