Skip to main content
Glama

Manos

A CLI Model Context Protocol 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 for the design rationale and roadmap.

Install

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. Working on manos itself? See 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.

Related MCP server: MCP Android Agent

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

claude mcp add manos -- npx -y manos-mcp serve
claude mcp list            # verify

Edit claude_desktop_config.json (macOS ~/Library/Application Support/Claude/, Windows %APPDATA%\Claude\) and restart the app:

{
  "mcpServers": {
    "manos": { "command": "npx", "args": ["-y", "manos-mcp", "serve"] }
  }
}

.cursor/mcp.json (project) or ~/.cursor/mcp.json (global), then enable manos under Settings → MCP:

{
  "mcpServers": {
    "manos": { "command": "npx", "args": ["-y", "manos-mcp", "serve"] }
  }
}

.vscode/mcp.json — note the servers key and explicit type:

{
  "servers": {
    "manos": { "type": "stdio", "command": "npx", "args": ["-y", "manos-mcp", "serve"] }
  }
}

~/.codeium/windsurf/mcp_config.json, then hit Refresh in the Windsurf MCP panel:

{
  "mcpServers": {
    "manos": { "command": "npx", "args": ["-y", "manos-mcp", "serve"] }
  }
}

Any MCP-capable client speaks the same stdio protocol. Configure a server that runs:

command: npx
args:    ["-y", "manos-mcp", "serve"]
transport: stdio

From source

For working on manos itself, or to pin a local build instead of the published package:

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

  • 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

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

Available Tools

40 tools
a11y_auditAccessibility auditA

Audit the current screen for accessibility issues: undersized touch targets, unlabeled interactive controls, and duplicate labels. (Color-contrast needs pixels and is not evaluated.)

Inputs: device_id (required).

assertAssertA

Assert an element is visible or not visible on the current screen. Returns pass/fail without polling (use wait_for to wait).

Inputs: device_id (required), id, text, index, +3 more.

cheat_sheetMaestro cheat sheetA

Return Maestro flow syntax guidance and best practices for writing/exporting flows.

Inputs: no named parameters.

clear_app_stateClear app stateA

Reset app state. Android: full data wipe (pm clear). iOS: resets permissions (full data wipe needs reinstall).

Inputs: app_id (required), device_id (required).

device_capabilitiesDevice capabilitiesA

Report which actions are supported on a device (full/partial/unavailable) with the backend and any caveats. Check this before relying on a platform-specific action.

Inputs: device_id (required).

export_flowExport flowA

Export the recorded session as a replayable Maestro flow (YAML) plus a human-readable report. Run it later with run_flow.

Inputs: no named parameters.

export_reportExport session report (HTML)A

Write a self-contained HTML report of the recorded session — a step timeline with screenshots (if report capture was on) plus an appendix with the replayable flow, captured network requests, and recent logs/crashes. Returns the file path. Best with start_recording(report=true).

Inputs: output_path, include_logs, include_network.

find_elementsFind elementsA

Search the current screen for elements matching a query. Returns matching elements (id, bounds, text, etc.) — handy to disambiguate before acting.

Inputs: device_id (required), id, text, index, +3 more.

find_textFind text (OCR)A

OCR the current screenshot and return on-screen text with pixel bounding boxes — finds elements the accessibility tree misses (styled buttons, canvas/Flutter/game UIs, WebViews). Optionally filter by a query. Tap a result with tap{text, ocr:true}.

Inputs: device_id (required), query, min_confidence.

get_logsGet device logsA

Fetch recent device logs (Android logcat / iOS unified log) with automatic crash & ANR detection. Filter by app_id, substring, time window, and line cap.

Inputs: device_id (required), app_id, filter, max_lines, +1 more.

input_textInput textA

Type text into the focused field. Optionally target a field first (id/text/resource_id) to tap it before typing.

Inputs: text (required), device_id (required), id, index, +4 more.

inspect_screenInspect screenA

Get the current screen's view hierarchy as compact JSON with stable element ids. Use the 'id' field with act tools (tap/input_text/...), or target by text/resource-id. Copy text verbatim — never retype from a screenshot.

Inputs: device_id (required).

launch_appLaunch appA

Launch an app by bundle id (iOS) / package name (Android). Optionally clear state first.

Inputs: app_id (required), device_id (required), observe, clear_state.

list_devicesList devicesA

List all available local devices (Android emulators/devices via adb, iOS simulators via simctl). Returns id, platform, name, OS version, and state.

Inputs: no named parameters.

long_pressLong pressA

Long-press an element or coordinates for a duration (ms).

Inputs: device_id (required), x, y, id, +7 more.

network_clearClear captured requestsA

Clear the captured-request buffer without stopping the capture.

Inputs: device_id (required).

network_mockMock network responsesA

Override API responses to test hard-to-reproduce states (Android). Each rule matches requests by URL regex (+ optional method) and can override the status/headers, replace the body, regex-rewrite the live body (change one field, keep the rest), or inject latency. First match wins; rules hot-reload; capture keeps logging (mocked exchanges flagged). Android (Frida/OkHttp): call network_start with app_id first — mocking rides the capture hook (abort not yet supported). iOS response mocking is in development (network_start still captures). replace:true (default) replaces all rules; an empty list clears mocking.

Inputs: device_id (required), rules, replace.

network_requestsGet captured requestsA

Return captured HTTP exchanges (method, url, status). Compact by default; set include_headers/include_body for detail. Filter by URL regex and limit count to avoid flooding.

Inputs: device_id (required), limit, filter, since_ts, +2 more.

network_startStart network captureA

Capture decrypted HTTP, filtered to specific endpoints. Android: hooks OkHttp via Frida in a debuggable app (app_id required; needs frida-server). iOS simulator: routes the sim through mitmproxy with a simctl-trusted CA (app_id optional; temporarily sets the macOS proxy, restored on stop). filter is a URL regex (e.g. 'v2/search').

Inputs: device_id (required), spawn, app_id, filter.

network_stopStop network captureA

Stop capturing (detach Frida / stop mitmproxy and restore the macOS proxy).

Inputs: device_id (required).

press_keyPress keyA

Press a hardware/system key. Android: back, home, enter, tab, delete, search, menu, volume_up, volume_down, app_switch, power. iOS: enter, delete, tab, space, escape, home, lock, siri.

Inputs: key (required), device_id (required), observe.

push_notificationPush notificationA

Deliver a push notification. iOS: injects an APNs payload via simctl push. Android: unavailable (needs FCM server key + token).

Inputs: app_id (required), payload (required), device_id (required).

run_flowRun Maestro flowA

Run a Maestro flow (delegates to the maestro CLI). Provide exactly one of: yaml (inline), files (paths), or dir. Optional include_tags/exclude_tags (for dir) and env vars.

Inputs: dir, env, yaml, files, +3 more.

set_appearanceSet appearanceB

Switch the system appearance between light and dark mode.

Inputs: device_id (required), appearance (required).

set_conditionsSet device conditions (preset)A

Apply multiple device conditions in one call — the edge-case states where bugs hide. Use a named preset and/or explicit fields (explicit overrides the preset). Each condition reports applied/skipped/failed (unsupported ones are skipped with a reason, not a failure). Presets: reset (Restore defaults: light mode, 1.0 text, online.); screenshot (Clean marketing state: light mode + a pristine 9:41 status bar.); accessibility (Accessibility stress: largest text + dark mode.); offline (No connectivity (airplane mode / radios off).); dark (Dark appearance.); international (Foreign locale (defaults to fr-FR; override with locale). Needs app_id.).

Inputs: device_id (required), app_id, locale, preset, +5 more.

set_font_scaleSet font scaleA

Set text size for accessibility testing. scale 1.0 = default. Android sets font_scale directly; iOS maps to the nearest Dynamic Type size.

Inputs: scale (required), device_id (required).

set_localeSet localeA

Set app locale (BCP-47, e.g. 'fr-FR'). Android: per-app, requires API 33+. iOS: applied by relaunching the app. Provide app_id.

Inputs: locale (required), device_id (required), app_id.

set_locationSet GPS locationA

Set the simulated GPS location. Android: emulator only. iOS: any simulator.

Inputs: latitude (required), device_id (required), longitude (required).

set_networkSet network conditionA

Toggle wifi/cellular/airplane mode. Android only (emulator/device); iOS simulators share the host network (use Network Link Conditioner).

Inputs: device_id (required), wifi, cellular, airplane_mode.

set_orientationSet orientationA

Set device orientation (portrait, landscape-left, landscape-right, upside-down). Android only; iOS simulators must be rotated via the Simulator menu.

Inputs: device_id (required), orientation (required).

set_permissionSet permissionA

Grant or revoke a runtime permission for an app. Aliases: camera, microphone, location, contacts, photos, notifications, calendar, etc.

Inputs: app_id (required), device_id (required), permission (required), grant.

set_status_barOverride status barA

Override the status bar (time, battery, signal) for clean screenshots. iOS: rich (simctl). Android: clock/battery/signal via SystemUI demo mode.

Inputs: device_id (required), time, wifi_bars, battery_level, +3 more.

start_recordingStart recordingA

Begin recording subsequent act tools as a session that can be exported to a replayable Maestro flow (export_flow) or an annotated HTML report (export_report). Set report=true to also screenshot after each action for the report.

Inputs: app_id, report.

stop_appStop appC

Force-stop / terminate an app.

Inputs: app_id (required), device_id (required).

stop_recordingStop recordingA

Stop recording the current session (keeps recorded steps for export_flow).

Inputs: no named parameters.

swipeSwipeA

Swipe by direction (up/down/left/right) or between two coordinate points. Direction swipes are centered on the screen.

Inputs: device_id (required), x1, x2, y1, +4 more.

take_screenshotTake screenshotA

Capture a PNG screenshot of the current device screen.

Inputs: device_id (required).

tapTapA

Tap an element (by id/text/resource_id/accessibility) or absolute coordinates. Returns the resulting screen state (act+observe) so you don't need a separate inspect call.

Inputs: device_id (required), x, y, id, +6 more.

wait_forWait forB

Poll the screen until an element becomes visible or not_visible, or timeout. Replaces fixed sleeps — fast when ready, patient when not.

Inputs: device_id (required), id, text, index, +5 more.

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

Loading tool schema changelog…

    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
    ResponsivenessOutdated data

    Related MCP Connectors

    Related MCP Servers

    • A
      license
      B
      quality
      B
      maintenance
      A Model Context Protocol server that enables scalable mobile automation through a platform-agnostic interface for iOS and Android devices, allowing agents and LLMs to interact with mobile applications using accessibility snapshots or coordinate-based interactions.
      35
      33,974 npm
      10,383
      Apache 2.0
    • A
      license
      Not graded
      quality
      D
      maintenance
      A Model Context Protocol server that enables AI agents to control and automate Android devices through natural language, supporting actions like app management, UI interactions, and device monitoring.
      59
      MIT
    • A
      license
      A
      quality
      D
      maintenance
      A Model Context Protocol server that enables scalable mobile automation for iOS and Android through a platform-agnostic interface, allowing LLMs to interact with mobile applications via accessibility snapshots or screenshot-based inputs.
      19
      33,974 npm
      2
      Apache 2.0