Files
agentic-mobile-control/docs/superpowers/specs/2026-07-21-host-agent-mcp-server-design.md
T
q792602257andClaude Opus 4.6 2f8f4a36c6 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 <noreply@anthropic.com>
2026-07-21 13:47:15 +08:00

24 KiB

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),配置长这样:
    mcp_servers:
      apex_device:
        url: "http://127.0.0.1:8765/mcp"
        headers:
          Authorization: "Bearer <token>"
    
  • 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=<host_agent_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

@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

@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

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()

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_trackerapp.mount("/mcp", auth_wrapped(server.streamable_http_app()))/api/statusmcp_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 <path>"
  ├─ 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:<prefix>" 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 随进程退出消失 不需显式清理

错误返回格式

{
  "jsonrpc": "2.0",
  "id": "<request-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:110tests/test_mcp.py:13,56tests/test_skill_catalog_e2e.py:287)都已显式传 manager=manager,改动面为 0。
  • Q2mcp-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 能及时暴露。
  • R6mcp 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:

mcp_servers:
  apex_device:
    url: "http://127.0.0.1:8765/mcp"
    headers:
      Authorization: "Bearer <token-from-host_mcp_token.json>"

首次启动 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