This document describes the cross-cutting design that every MCP in this monorepo follows.
Every package under packages/* is structured the same way:
domain/ pure business logic — entities, Result, Failures, repository protocols, use cases
infrastructure/ outbound adapters — subprocess wrappers, library wrappers, no domain knowledge
data/ parsers + repository implementations + composites
presentation/ MCP server — tool registry maps tool names to use cases
adapters/ optional HTTP/REST/OpenAPI surfaces for autonomous-agent consumption
container.py composition root — wires every concrete repo into every use case
Dependency direction is inward. Domain depends on nothing in the project. Data depends on domain + infrastructure. Presentation depends on domain. The composition root depends on everything.
Every use case returns Result[T] = Ok[T] | Err. Every Failure carries a canonical next_action string so an agent (Claude or a local LLM) can switch on it without parsing the message:
next_action |
Meaning |
|---|---|
"run_check_environment" |
Env preflight should fix it |
"fix_arguments" |
Schema/argument problem |
"wait_or_force" |
A lock is held; wait or force |
"force_release_lock" |
Stale lock; admin action |
"calibrate_stand" |
AR/vision specific |
"review_diff" |
Visual-diff failure |
"ask_user" |
Needs human decision |
"retry_with_backoff" |
Transient |
The MCP layer translates Result into a uniform {ok, data, error: {code, message, next_action, details}} envelope on both stdio and HTTP transports.
Two orthogonal composites in phone-controll:
- Platform composite — routes by
Platform(Android / iOS) for device, lifecycle, UI, observation calls. Backed byCachingPlatformResolverthat remembersserial → (Platform, DeviceClass). - Framework composite — routes by
TestFramework(Patrol / Flutter / future XCUITest / Espresso / Detox / Playwright) for test execution. Backed byProjectInspectorthat determines which frameworks apply to a project.
iOS has a third level: MultiSourceIosDeviceRepository unions physical (pymobiledevice3) and simulator (xcrun simctl) sources, routing each call by DeviceClass.
Every Claude conversation spawns its own MCP subprocess. Coordination across processes:
- Filesystem device locks —
~/.mcp_phone_controll/locks/<serial>.lock, JSON contents (session_id, pid, started_at),O_EXCLcreation, PID-aware stale reclaim.select_deviceacquires;release_devicereleases. - No shared state otherwise — each MCP process has its own session trace, selected device, session id.
For an HTTP shared-server topology (one process serving N Claude clients), swap FilesystemDeviceLockRepository for InMemoryDeviceLockRepository; nothing else changes.
run_test_plan interprets apiVersion: phone-controll/v1 plans:
phases: PRE_FLIGHT → CLEAN → LAUNCHED → <NAME>_GATE → UNDER_TEST → VERDICT_*
drivers: patrol_test | flutter_test | tap_text | noop
Phase failures auto-inject VERDICT_BLOCKED. Planned-decline branches stop the walker after the verdict phase. Schema is self-describing via describe_capabilities.plan_schema — agents author plans without trial-and-error.
- stdio MCP (default) —
python -m mcp_phone_controllregistered viaclaude mcp add. - OpenAI-compat HTTP adapter (optional) —
mcp-phone-controll-http. ExposesGET /tools(OpenAI function-call schemas),POST /tools/{name}(dispatcher passthrough),GET /openapi.json(free FastAPI). Identical envelopes to stdio.
A future MCP can add its own sub-router (e.g. /dev-session/*) without forking the dispatcher.
- Native macOS for human-developer machines: real USB devices, iOS Simulator, GUI VS Code, multi-Claude concurrency.
- Linux container for CI: headless Android emulator + Flutter + Patrol + MCP. iOS Simulator cannot be containerised (Apple-side restriction).
The container image is documented in docs/adding_an_mcp.md § "CI image" but not yet built.
Adding a new test framework, a new platform's UI driver, a new fiducial-marker dictionary, or a new MCP package — none of these change use cases or tool definitions. Every extension is "implement the protocol, register in the container, add to the framework-runner map." The composite layer absorbs the routing.