Releases: iksnerd/adb-mcp
Release list
v0.20.0
Project renamed adb_mcp → adb-mcp, including the Go module path.
The CLI binary, cmd/adb-mcp, and every tool description already used the
hyphenated form; the module path and GitHub URLs were the odd ones out. The
GitHub repo itself is now iksnerd/adb-mcp (the old adb_mcp URL
redirects). If you go installed a previous version, re-run install with
the new path — go install github.com/iksnerd/adb-mcp/cmd/adb-mcp@latest —
since a module's declared path must match the path it's fetched under, and
v0.19.0 was tagged under the old name.
gradle_project_properties redacts secret-shaped values by default.
Gradle's stock properties task dumps a module's entire effective property
set, which routinely includes credentials injected via
~/.gradle/gradle.properties or env for private Maven repo auth —
indistinguishable in the raw output from harmless entries like
compileSdkVersion. Property values whose key matches
password/token/key/credential (case-insensitive) are now masked before the
tool returns them. Field report: council-hub android-mcp-papercuts
#019fdd06.
list_gradle_variants / list_gradle_tasks can now scope to a
submodule. Both previously always ran against the Gradle root —
list_gradle_tasks silently ignored its own task parameter — so a
multi-module build where only a submodule applied the Android plugin got
"No build variants found" even though the submodule plainly had them. Both
now honor task="<module>:tasks" the same way gradle_build's task=
already does.
wait_for_text's timeout message now points at the viewport caveat.
Android's accessibility tree only covers the current viewport (see
v0.19.0's scroll option below), but the timeout error still read as
unconditional absence. It now says so and suggests scroll=true when the
call didn't already use it.
Full Changelog: v0.19.0...v0.20.0
v0.19.0
logcat / start_logcat_capture / stop_logcat_capture get redact:true.
Logcat output routinely gets copied into transcripts and bug reports, and
payment/auth SDKs log tokens, passwords, and API keys in plain text. Passing
redact: true masks common secret-shaped values (token, password,
authorization, api_key, private_key, encryption_key, and
Authorization:-style headers) before the lines leave the MCP response. Off
by default — opt in when driving a build that logs credentials.
launch_dev_client reports registered URL schemes on failure. A wrong
scheme argument previously just failed with OpenURL's raw error or the
generic Metro-unreachable message, leaving the actual fix ("what scheme does
this app register?") as a manual dumpsys dig. Both failure paths now query
dumpsys package for the app's declared URL scheme(s) and fold them into the
error text when found.
describe_ui gets an optional package ownership check. The header
already named the focused top window; passing package now compares it
against the caller's expected package and adds an explicit warning line when
they don't match, instead of leaving a silent wrong-app read to be noticed by
eye.
wait_for_text gets opt-in scroll. Android's accessibility tree only
covers the current viewport, so waiting on text below the fold inside a
ScrollView could time out even though the text would appear on scroll. With
scroll: true, each poll also swipes upward through the screen to surface
off-screen rows. Default stays off, matching existing polling semantics.
Full Changelog: v0.18.0...v0.19.0
v0.11.2
Full Changelog: v0.11.1...v0.11.2
Full Changelog: v0.11.1...v0.11.2
v0.11.1
Full Changelog: v0.11.0...v0.11.1
Full Changelog: v0.11.0...v0.11.1
v0.11.0
Full Changelog: v0.10.1...v0.11.0
Full Changelog: v0.10.1...v0.11.0
v0.18.0
Round 9 field feedback (android-mcp-papercuts #019fdb7d, 2026-08-07 — a
timing-sensitive app-exit/reopen bug), plus the remaining XcodeBuildMCP parity
gaps. Every change here went through an independent code review before
shipping; six real bugs it found (three correctness, one injection risk, one
race, one dead-code no-op) were fixed and each has a regression test — see
below.
app_state gets foreground/top_activity. running: true was equally
true for a backgrounded process, so it couldn't answer the actual pass/fail
question behind a foreground/background bug ("is the app on screen, or did it
fall back to the launcher"). Now parses dumpsys activity activities for the
authoritative topResumedActivity= line (falling back to a per-stack
mResumedActivity: line on older Android versions that don't print it).
app_state gets a Metro-staleness signal. A live Metro connection doesn't
mean the running JS is current — Metro's file watcher misses git stash/git checkout file replacement, so bundle_source: metro can be quietly stale and
produce confidently wrong conclusions. Pass source_path to compare the
newest host source mtime against the latest Metro/HMR marker timestamp; the
response gains bundle_stale, source_mtime, and last_hmr_update.
run_sequence gets assert_foreground and per-step elapsed_ms. A
timing-sensitive repro (a ~1-2s reproduction window) couldn't live entirely in
one sequence call without a foreground check, or report whether a slow run
blew the window instead of silently passing. Both now available as sequence
steps/fields.
launch_dev_client detects the Metro-unreachable error screen. Expo's dev
launcher returns success from am start even when it lands on
DevLauncherErrorActivity (Metro unreachable). The tool now checks the
settled foreground activity after launch and surfaces the on-screen error
text instead of a false "launched" report.
New — gradle_project_properties and scaffold_android_project
(the remaining XcodeBuildMCP parity gaps). The former dumps a module's
evaluated Gradle properties (namespace, SDK settings, build dir) via its
:properties task; the latter creates a minimal Kotlin Android project
(Gradle Kotlin DSL, manifest, launcher Activity, resources) in a new empty
directory — run gradle wrapper there before gradle_build.
New — prefer_pin. Best-effort nudge of a standard BiometricPrompt toward
its PIN/password fallback (an explicit system button, or BACK as the generic
path) — apps can suppress or rename the fallback, so confirm the result with
describe_ui.
Fix — describe_ui's auto filter now actually collapses single-child
wrapper chains. The chain-collapse rule added for this (round 4) required
an empty resource id to fire, which made it redundant with the existing
fallback return — it never changed output. It no longer requires an empty
resource id, so label-less, non-clickable single-child wrappers collapse even
when they carry a resource id (Material's nested navigation_bar_item_*
containers, the original complaint).
Code health. Deduped the "is this package installed?" check
(isPackageInstalled, shared by GetAppStateWithSource and GetAppDetails)
and unified the logcat-capture/screen-recording session bookkeeping into one
generic sessionRegistry[T] in internal/adb/capture.go, replacing two
near-identical map[string]*T + mutex pairs.
Bugs found and fixed during review, before any of this shipped:
app_state's Metro-staleness check compared against the oldest HMR
marker in the logcat window instead of the latest (logcat is chronological)
— would have false-positivedbundle_staleon a freshly-updated bundle.scaffold_android_project's project name was interpolated unescaped into
generated Kotlin build scripts that Gradle executes — a crafted name was a
code-injection path. Names are now validated against a safe charset.- A name whose first word started with a digit produced an invalid Kotlin
class identifier (123Activity); now prefixed (App123Activity). parseResumedActivitypreferred whichever oftopResumedActivity=/
mResumedActivity:appeared first in the raw dump, which could pick the
wrong app's activity on split-screen/multi-display devices; now always
prefers the authoritative global line.launch_dev_clientchecked the foreground activity immediately after
firing the launch intent, racing the activity transition; now polls via a
newSettledResumedActivity(mirrors the existingdescribeSettled
settle-and-compare pattern) instead of a single immediate read.
Full Changelog: v0.17.1...v0.18.0
v0.17.1
Field report (2026-08-05): on modern Expo/RN builds, app_state's
Metro-vs-embedded verdict relied on logcat markers (HMRClient/Fast Refresh/DevServerHelper) that some current dev clients don't emit, leaving
the bundle source unknown even when the app was live-connected to Metro.
Fix — a second, independent signal. When logcat gives no verdict,
app_state now reads the app process' /proc/<pid>/net/tcp[6] for an
established connection to a conventional Metro/Expo dev-server port (8081,
8082, 19000–19002) and reports metro from that instead. The response gains
bundle_signals (logcat and/or live_socket) showing which evidence the
verdict is based on. Logcat stays the primary signal — a live socket proves a
dev-server connection but not that the app is React Native — so the fallback
only fires when logcat is inconclusive. Unit-tested against real
/proc/net/tcp line formats.
Also closed out several open TODO/BACKLOG items to reflect current state: the
Maestro integration question is decided (defer — run_sequence covers batching),
batch-tap is folded into run_sequence, and the accessibility-action-tap and
Expo-dev-client-verification items are reframed as known open gaps rather than
untouched backlog.
Full Changelog: v0.17.0...v0.17.1
v0.17.0
A performance fix and two backlog items.
Perf — screenshot decodes the PNG once, not twice. CaptureScreen decoded
each frame in isMostlyBlack and again in downscalePNG (and re-decoded on
every black-retry). Measured: ~85 ms + 18 MB per png.Decode of a 2076×2152
frame. It now decodes once into an image.Image and shares it between the
black-check and the downscale — roughly halving the CPU/allocs of the
most-called tool (screenshot runs after every action in the driving loop). The
byte-in wrappers are kept for the tested undecodable-input path; verified live
that full-res and downscaled captures still decode correctly.
set_battery works on physical devices (closes the enhancement backlog).
The emulator path (adb emu power) is unchanged; a physical device now forces
the values through the framework (dumpsys battery set level/ac). Those persist
until you clear them, so a new reset option restores automatic reporting
(dumpsys battery reset) on either. Verified live that set/reset round-trip.
New — list_gradle_projects (XcodeBuildMCP parity — deeper project
discovery). Runs gradlew projects and returns the module paths of a
multi-module build (:app, :core, :feature:login) so you can point
gradle_build/list_gradle_variants at the right module or address a task with
:module:task. Complements list_gradle_variants (which lists a module's build
variants). Parser unit-tested against the standard gradlew projects tree.
Full Changelog: v0.16.0...v0.17.0
v0.16.0
Four field-feedback items, each reproduced and verified on a live emulator
(emulator-5554, including a Pixel_10_Pro_Fold AVD for the foldable case).
Fix — screenshot on multi-display foldables. On a device with more than one
physical display, screencap -p (no -d) prints a [Warning] Multiple displays were found … line to stdout ahead of the PNG, shifting the header ~250–350
bytes so nothing can decode it — the capture came back as 0x0 / "dimensions
could not be read", indistinguishable from a blank or FLAG_SECURE frame. On a
foldable the tool was effectively 100% unusable. Fix: strip any leading bytes
before the PNG signature (\x89PNG) from every screencap — robust and
display-agnostic (harmless on single-display, where the signature is already at
offset 0). Also added an optional display param ("inner"/"primary",
"cover"/"outer", an HWC index, or a raw physical id) to grab a specific
panel. Note: screencap -d keys off the physical display id from
dumpsys SurfaceFlinger --display-id, not the logical id 0/1 (passing the
logical id makes screencap fail outright) — ResolveDisplay handles the
mapping. (android-emulator-mcp-feedback #019f7abc.)
New — app_state. The most-requested gap: no way to tell a Metro-connected
dev process from one silently running its embedded bundle (which ignores
every JS edit), or to notice two live processes for one package (taps and log
reads hitting different pids). Reports installed?/running? + pid(s), main-process
uptime, first-install/last-update times, and a Metro-vs-embedded bundle
source heuristic over the app's recent logcat (HMRClient / Fast Refresh /
DevServer markers), with the evidence line it keyed on. Run it first when JS
edits seem to have no effect. (android-mcp-papercuts #019f6fad item 4.)
New — has_biometric_enrolled. Reports whether any fingerprint is enrolled
(and how many) from dumpsys fingerprint. Check it before a biometric flow:
with nothing enrolled, fingerprint_touch can never satisfy a BiometricPrompt —
it just sits on "Touch the sensor". Design settled by earlier live probing
(round 7): the framework exposes only an enrolled count, never the finger id,
and a wrong fingerprint_touch id trips a HAL lockout — so this is a count
probe, not runtime id-discovery. Verified live: an empty AVD reports 0, and 1
after enrolling one fingerprint. (android-mcp-papercuts #019f709b, reframed.)
New — run_sequence (resolves the round-4 DECISION). Runs several steps in
one call — sleep, tap, tap_text, tap_element, key, text, swipe,
launch, stop, wait_text, describe_ui — with if_present/if_absent
guards (the conditional-cancel idiom) and per-step optional. The point isn't
just fewer round-trips: for flows gated on native timing (a background-token
clear, a biometric prompt that auto-fires on resume) a per-step agent round-trip
perturbs the very timer being tested, so batching is the only faithful way to
reproduce them. Returns a per-step result (ok/skipped/error) plus the final
hierarchy; a non-optional step error stops the rest. Verified live end-to-end
(home → launch → wait_text → guarded steps → describe_ui). The larger Maestro
integration stays a separate open decision. (android-mcp-papercuts #019f6fad /
addendum #019f6fb4.)
Docs & guides. driving gains the foldable-capture note, an "edits not
showing up → check app_state for the embedded-bundle trap" gotcha, and a
run_sequence note for native-timer flows;
pin-and-lock leads the biometric section with has_biometric_enrolled; the
fingerprint_touch / adb_reverse / reload_app tool descriptions cross-link
the new tools.
Full Changelog: v0.15.1...v0.16.0