Skip to main content
Glama
README.md
# 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

A3.5/5.0

Scored across 40 tools

Disambiguation5/5

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.

Naming Consistency3/5

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.

Tool Count2/5

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

Completeness4/5

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.

Maintenance

ActivityInactive
ResponsivenessNo issues