A local-first MCP server that lets Codex build, inspect, test, and control Android emulators or explicitly allowed USB devices through adb.
Strong tool definitions with comprehensive descriptions (avg 250+ chars) and detailed parameter documentation. All 10 tools have explicit schemas with typed parameters. Naming follows verb_noun convention (android_*). However, output schemas are not formally documented in the source, and error handling guidance is minimal. No tool annotations (readOnlyHint/destructiveHint) despite clear risk levels. Parameter descriptions are excellent but lack enum constraints where applicable (e.g., mode in android_logs, priority levels). Missing idempotency guarantees and recovery guidance for destructive operations.
Dump package metadata via `dumpsys package <pkg>`: version, SDK levels, install paths, timestamps, installer, and whether the package is system/running. A package that is NOT installed is a normal answer (`installed:false` + a note), not an error — the caller asked a question and got a fact.
Dump the stack trace of a running process. Pass either the package name (resolved to a pid via `pidof -s`) or the pid directly. The tool asks ART to dump every thread's stack into `/data/anr/` via `kill -3 <pid>`, then reads the newest trace file. On a production phone, both steps usually fail (the adb shell user cannot signal most processes, and `/data/anr` is not world-readable), so the tool degrades to the `crash` log buffer and reports the engine used. An empty backtrace is never returned — the engine field always names what produced the lines, and a `note` field explains what the engine could NOT do.
Build and install an app from source using the Android Gradle Plugin (AGP), then launch it. Requires a Gradle project with an AGP-managed app module in the current working directory or a parent. The tool detects the module, resolves its package name from the AGP manifest, builds a debug APK, installs it on the device, and launches the LAUNCHER activity — all in one call. Pass either the module name (e.g. "app") or omit it to auto-detect a single app module. Gradle and the Android SDK must be on PATH or discoverable via ANDROID_HOME. The build output is streamed to stderr so you can watch the compilation. On success, the installed package name and launch result are returned; on failure, the build error is thrown.
Output schemas not formally documented. Tool descriptions explain what is returned (e.g., 'TOTAL PSS plus App Summary categories' for android_meminfo) but JSON Schema output types are absent from visible source. LLMs cannot plan downstream tool chains without knowing return field names and types.
No tool annotations (readOnlyHint, destructiveHint, idempotentHint) despite clear risk levels. android_launch_app, android_build_run, and android_tap_row are marked WRITE/destructive but lack annotation metadata. Agents cannot distinguish safe read-only tools from state-modifying ones without explicit hints.
| Scored | Grade | Overall | Spec posture | Rubric |
|---|---|---|---|---|
| 2026-09-23 | C | 62 | 2026-07-28+ | v2 |
Launch an already-installed app by resolving its LAUNCHER activity (`monkey -p <pkg> -c android.intent.category.LAUNCHER 1`) — no shell needed. Pass EITHER packageName OR name (exactly one): `name` is a case-insensitive fragment of the package name, resolved against the same listing android_list_apps returns, so several matches come back as a candidate list rather than a guess. Package names of third-party apps CANNOT be guessed; run android_list_apps first when unsure. Use this to OPEN an app; android_build_run is for building and installing one from source. Stable AOSP/GMS packages: Settings com.android.settings, Chrome com.android.chrome, Clock com.google.android.deskclock, Calendar com.google.android.calendar, Camera com.android.camera2, Files com.google.android.documentsui, Play Store com.android.vending.
List the packages INSTALLED on a device (`pm list packages`), enriched with versionName and whether each is a preinstalled system package. Run this BEFORE opening an app: a package name cannot be guessed — a plausible-looking id (a former app name, or the pattern a sibling app uses) is routinely not the installed one. Filter with `query`, a case-insensitive substring matched against the PACKAGE NAME. Android exposes no display label over adb (an app's android:label lives in compiled resources that need aapt2 from the SDK), so a Chinese/Japanese label read off the screen will NEVER match here — match a package fragment, or tap the icon with android_find_text + android_tap_text. User-installed packages only by default; include_system:true adds the preinstalled ones. A listing that FAILS throws with the reason instead of returning an empty list, so count:0 always means the device really has no matching package. Concurrency-safe.
Capture logcat output in two bounded modes: `snapshot` (the recent persisted ring from the device clock) or `follow` (a live window that returns once duration_seconds elapses). Output is capped while capturing (~300 lines / ~30 KB) so a chatty device can never drown the model. The spawned adb child is reaped on window close, on abort, and on the hard safety deadline through a process-GROUP kill.
Dump memory usage of a running process via `dumpsys meminfo <pkg>`: TOTAL PSS plus the App Summary categories (Java heap, native heap, code, stack, graphics). Pass either the package name (resolved to a pid via `pidof -s`) or the pid directly. The tool returns the memory breakdown and notes when the process was not running or the dump was partial.
List the processes running on a device (`ps -A`): pid and process name. App processes are named after their package (or `<package>:<process>` for a declared sub-process), so this is how you confirm an app is actually running and find the pid other tools want. Filter with a case-insensitive substring. A failed listing throws with the reason rather than returning an empty list.
Tap at a RELATIVE position inside a row (fractions of the row frame) instead of guessing absolute screen coordinates, and optionally verify the action by checking that a counter moved the expected ±1. A tap is planned from a FRESH dump, an out-of-range row index FAILS instead of clamping, an `expect_count` key the row's counters do not contain is refused BEFORE any tap (that is exactly the probe-click failure mode on a real account), and verification reports unverified-with-reason instead of guessing.
Read the visible list/feed rows of the frontmost app as ROWS instead of a raw view tree: each row carries an index, its frame in display PIXELS, the aggregated label (every text and content-desc inside the row subtree), and the counters parsed out of that label — number + classifier, e.g. "57 回复" → 回复=57, in 中文 or English. Use this for RecyclerView / ListView / LazyColumn screens: a list item is a SUBTREE, not an element, and its per-item controls (like, bookmark, share) are usually unlabeled ImageViews with no resource-id — android_tap_element cannot reach them, but android_tap_row taps INSIDE a row at a relative position (see its expect_count). A row is a run of three or more sibling subtrees that share a class and a near-equal height, and at least one of them carries a label; nested runs collapse to the outermost. Counters are parsed heuristically and their keys round-trip, so pass a key EXACTLY as returned to android_tap_row.expect_count. When no rows are found the hint says WHY (the rows are scrolled out of view / the screen exposes no labels at all / this is not a list screen) — never a blanket claim about the app. Rows scrolled off the display are excluded and counted as omittedOffscreen.
Enum constraints missing for constrained parameters. android_logs accepts 'mode' (snapshot|follow), 'priority' (V D I W E F), 'buffer' (main, system, crash, events, radio, all) but these are documented as free-form strings, not enums. LLMs will hallucinate invalid values.
Error handling lacks recovery guidance. Descriptions mention failures (e.g., 'A failed listing throws with the reason') but do not guide agents on next steps. No pattern like 'If device is offline, try android_devices first' or 'If package not found, run android_list_apps to discover installed apps.'
Mutual exclusivity not enforced in schema. android_launch_app accepts both 'packageName' and 'name' with text 'passing both is an error' but JSON Schema does not declare oneOf constraint. Agents may pass both, causing runtime failure instead of early validation.