Manos MCP
# Manos
A CLI [Model Context Protocol](https://modelcontextprotocol.io) server for **ad-hoc UI testing of Android & iOS apps** — purpose-built for the exploratory, test-free loop where an LLM agent pokes at an app and reacts to what it sees.
It controls Android emulators/devices (`adb`) and iOS simulators (`xcrun simctl`, with `idb` for native UI interaction) and exposes 39 tools over stdio.
manos gives an agent a tight **act → observe** loop, **device-condition control**, **crash/log capture**, **network capture** for debug builds, **OCR targeting** for off-tree elements, an **accessibility audit**, and **session recording** that promotes an ad-hoc exploration into a replayable regression test in one call. See [IMPROVEMENTS.md](IMPROVEMENTS.md) for the design rationale and roadmap.
## Install
```bash
npm install -g manos-mcp # install the CLI, or run on demand with: npx -y manos-mcp
manos doctor # check toolchain + list devices & capabilities
```
Requires **Node 20+**. Most MCP clients can launch manos on demand with `npx` — no global install needed; see [Register with an MCP client](#register-with-an-mcp-client). Working on manos itself? See [From source](#from-source).
| Backend | Used for | Install |
| --- | --- | --- |
| `adb` | all Android control | Android platform-tools (auto-detected from `$ANDROID_HOME` or the default SDK path) |
| `xcrun simctl` | iOS lifecycle, conditions, logs, screenshots, push | `xcode-select --install` |
| `idb` | fast native iOS UI inspect/tap/type | `brew install idb-companion && pipx install fb-idb` |
| `maestro` | under the hood: `run_flow`, the warm hierarchy engine, and the cross-platform inspect/interaction fallback | <https://maestro.dev> |
iOS UI interaction works **without** `idb` by falling back to Maestro (slower; call `launch_app` first). Android UI inspection falls back from `uiautomator dump` to `maestro hierarchy` automatically when the on-device UiAutomation connection is contended.
## Register with an MCP client
Each tab launches manos with `npx -y manos-mcp serve`, which fetches and runs the published package on demand — no clone or global install required. (If you installed globally with `npm install -g manos-mcp`, use a bare `manos serve` instead.)
<details open>
<summary><b>Claude Code</b></summary>
```bash
claude mcp add manos -- npx -y manos-mcp serve
claude mcp list # verify
```
</details>
<details>
<summary><b>Claude Desktop</b></summary>
Edit `claude_desktop_config.json` (macOS `~/Library/Application Support/Claude/`, Windows `%APPDATA%\Claude\`) and restart the app:
```json
{
"mcpServers": {
"manos": { "command": "npx", "args": ["-y", "manos-mcp", "serve"] }
}
}
```
</details>
<details>
<summary><b>Cursor</b></summary>
`.cursor/mcp.json` (project) or `~/.cursor/mcp.json` (global), then enable **manos** under Settings → MCP:
```json
{
"mcpServers": {
"manos": { "command": "npx", "args": ["-y", "manos-mcp", "serve"] }
}
}
```
</details>
<details>
<summary><b>VS Code (GitHub Copilot agent mode)</b></summary>
`.vscode/mcp.json` — note the `servers` key and explicit `type`:
```json
{
"servers": {
"manos": { "type": "stdio", "command": "npx", "args": ["-y", "manos-mcp", "serve"] }
}
}
```
</details>
<details>
<summary><b>Windsurf</b></summary>
`~/.codeium/windsurf/mcp_config.json`, then hit **Refresh** in the Windsurf MCP panel:
```json
{
"mcpServers": {
"manos": { "command": "npx", "args": ["-y", "manos-mcp", "serve"] }
}
}
```
</details>
<details>
<summary><b>Other / generic MCP client</b></summary>
Any MCP-capable client speaks the same stdio protocol. Configure a server that runs:
```
command: npx
args: ["-y", "manos-mcp", "serve"]
transport: stdio
```
</details>
## From source
For working on manos itself, or to pin a local build instead of the published package:
```bash
git clone https://github.com/ryanperkins/Manos-MCP.git && cd Manos-MCP
npm install # also builds via the prepare script
npm run build # or rebuild after changes
node dist/cli.js doctor
```
Register the local build by pointing your client's `command`/`args` at `node /ABS/PATH/to/Manos-MCP/dist/cli.js serve` instead of the `npx` form above.
## CLI
```
manos serve Start the MCP server on stdio (default)
manos doctor Toolchain + connected devices + per-device capabilities
manos devices List connected devices (tab-separated)
manos --help
```
## The tools
Full reference and the **Android vs iOS comparison matrix** are in [docs/index.html](docs/index.html). Highlights:
- **Core:** `list_devices`, `device_capabilities`, `inspect_screen`, `take_screenshot`.
- **Authored flows:** `run_flow` runs a declarative flow locally; `cheat_sheet` gives the syntax. `export_flow` (below) turns a recorded session into one.
- **Act + observe:** `tap`, `long_press`, `input_text`, `press_key`, `swipe` — each takes a selector (`id`/`text`/`resource_id`/`accessibility`) **or** coordinates and returns the resulting screen (`observe: screen | diff | screenshot | none`).
- **Smart waits / assertions / search:** `wait_for`, `assert`, `find_elements`, and `find_text` (OCR the screenshot to locate text the accessibility tree misses — styled buttons, canvas/Flutter/game UIs, WebViews). Targeting falls back to OCR automatically when a text selector finds nothing in the tree, or force it with `tap{text, ocr:true}`.
- **App state:** `launch_app`, `stop_app`, `clear_app_state`, `open_deeplink`, `set_permission`.
- **Device conditions:** `set_appearance`, `set_orientation`, `set_locale`, `set_network`, `set_location`, `set_font_scale`, `set_status_bar`, `push_notification`, and `set_conditions` (apply many at once / named presets like `offline`, `accessibility`, `screenshot`).
- **Diagnostics:** `get_logs` (with crash/ANR detection), `a11y_audit`.
- **Network capture (debug apps):** `network_start`/`network_requests`/`network_clear`/`network_stop` — capture decrypted HTTP filtered to specific endpoints. Android hooks OkHttp via Frida (works through HTTP/2, pinning, proxy-bypass); iOS Simulator uses mitmproxy + a `simctl`-trusted CA. See [NETWORK.md](NETWORK.md).
- **Network mocking (Android):** `network_mock` — manipulate live API responses to test hard-to-reproduce states: override `status`/`headers`, replace the `body`, **regex-`rewrite` a field in the live body** (keep the rest), or inject latency. Rule-based (URL regex + method), hot-reloads, no separate mock server or build change. Rides the Frida/OkHttp capture hook (per-process, no host-proxy disruption — `network_start` first). iOS response mocking is in development. See [NETWORK.md](NETWORK.md#mocking-responses-android--network_mock).
- **Recording:** `start_recording` → act → `export_flow` (replayable Maestro flow) or `export_report` (self-contained HTML report: screenshot timeline + flow + logs + captured network).
A typical loop:
```
list_devices → inspect_screen → tap{text:"Login", observe:"diff"} → input_text{...} → wait_for{text:"Welcome"}
```
## How element targeting works
`inspect_screen` returns a compact tree where every node has a **stable `id`** derived from its semantic identity (resource-id / accessibility / class + digit-normalized text), not its position. So a counter ticking from `5` to `6` keeps its id and shows up as a *changed* node in a diff, while a newly-appeared element is *added*. Act tools accept that `id`, a text/resource-id selector, or raw coordinates; when you use a selector, the recorded flow stores the selector (resilient replay) rather than brittle coordinates.
## Performance
Hierarchy reads use a three-tier backend, chosen per device:
1. **adb `uiautomator dump`** (~2.5s, no extra process) — the default on Android. Tried first.
2. **Warm hierarchy engine** — when uiautomator can't reach UI-idle (e.g. apps with constant animations/watermarks, where `uiautomator dump` errors with `could not get idle state`), manos keeps **one long-lived `maestro mcp` child** resident (used under the hood) and reuses its connected driver. First call pays a one-time warm-up; subsequent inspects are **~150–300ms** — the payoff of reusing a resident engine instead of cold-starting the JVM per call.
3. **Cold `maestro hierarchy` CLI** — last resort if the warm session can't start.
Once a device needs the warm session, manos remembers it (per-device) so it doesn't re-pay the uiautomator timeout on every inspect. Screen size/density are cached. The warm child (and its `simulator-server`) are killed via a process-tree cleanup on exit.
Measured on a hard case (an app that never idles, so everything routes through the warm session):
| | First inspect (one-time) | Steady-state inspect (median) | Full tap+observe loop |
| --- | --- | --- | --- |
| **manos** | ~11s | **~175ms** | **~3s** (was ~35s with cold per-call CLI) |
So: on apps where uiautomator works, the adb path is fast with no extra process; on apps that force the fallback, the resident warm engine keeps steady-state inspect in the **~175ms** range, and the act+observe loop avoids the per-action JVM cold-start that made it slow before. The remaining first-call cost is the one-time uiautomator probe before switching to warm.
## Architecture
```
src/
cli.ts serve / doctor / devices
server.ts McpServer wiring (stdio)
tools/
register.ts all 39 tools
context.ts shared state: resolveTarget + act/observe + last-screen cache
drivers/
types.ts Driver contract + Capability model
android.ts adb
ios.ts simctl + idb (+ maestro fallback)
registry.ts device → driver routing
core/
hierarchy.ts compact JSON, stable ids, screen diff, search
a11y.ts accessibility heuristics
waits.ts condition polling
session.ts action journal
flow.ts Maestro-flow emitter
maestro.ts maestro CLI passthrough (run_flow, cheat sheet)
maestroDriver.ts cold maestro hierarchy + one-shot action fallbacks
maestroSession.ts warm long-lived `maestro mcp` backend (fast hierarchy/actions)
netcapture.ts network capture (Frida OkHttp / mitmproxy) — see NETWORK.md
ocr.ts OCR fallback (Apple Vision / Tesseract) for off-tree elements
assets/frida/ okhttp-capture.js + sidecar.py (injected by netcapture)
util/ exec + toolchain discovery
```
Each driver method may throw a `CapabilityError`; tools surface it as an actionable message instead of an opaque subprocess failure. The capability model is reported live so an agent can check support before relying on a platform-specific action.
## Test
```bash
npm test # unit tests for hierarchy/diff/a11y/flow (no device needed)
```
The hierarchy parsing, stable-id diffing, accessibility math, screenshot capture, log/crash scan, and the recording→`export_flow`→`maestro check-syntax` pipeline have also been verified end-to-end against a live Android emulator.
## License
MIT
TDQS
Scored across 40 tools
Each tool targets a distinct action or resource with clear boundaries (assert vs wait_for, find_elements vs find_text vs inspect_screen, network_start vs network_mock). Overlapping purposes are differentiated by polling vs non-polling, OCR vs accessibility tree, and capture vs mock.
Most tools follow a verb_noun pattern, but the network_* group reverses it (network_start, network_stop, network_mock, network_clear), and several noun-style names (device_capabilities, cheat_sheet, a11y_audit) break the convention. This mix is readable but not uniform.
With 40 tools, the server is well above the 25+ 'too many' threshold. While each tool has a distinct testing purpose, the count is unwieldy for a single server and could be split into focused subdomains (device, network, recording, etc.).
The tool set covers the core mobile testing lifecycle: app launch/stop, UI interaction and inspection, assertions/waits, device conditions, network capture/mocking, flow recording/export, logs, and accessibility. Minor gaps like missing install/uninstall and explicit device selection are workarounds, not dead ends.