android-build-mcp
Provides tools for interacting with Android devices and apps, including building, testing, installing, launching, UI inspection, screen capture, input simulation, logging, and device management via adb.
Enables running Gradle builds and tests, with concise failure summaries and pre-build checks for JDK/Gradle compatibility and other known failure modes.
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@android-build-mcp The start button doesn't do anything on the tablet. Fix it."
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
android-build-mcp
An MCP server that gives a coding agent hands and eyes on an Android device.
Your agent can already shell out to gradlew and adb. The problem is what that costs: a failed build dumps four thousand lines of task progress into the context window, tap coordinates get guessed off a screenshot, and a JDK/Gradle mismatch sends it bisecting application code for an hour over an error that had nothing to do with the app.
This is the build-debug loop made cheap. 19 tools covering build, test, install, launch, screenshot, UI inspection, input, logs, and toolchain diagnosis — each one shaped to return the smallest thing that answers the question.
You: "The start button doesn't do anything on the tablet. Fix it."
Agent: android_doctor -> toolchain OK, Gradle 8.7 / JDK 17 compatible
android_build -> assembleDebug + install + launch
android_set_rotation -> landscape
android_dump_ui -> Button "start" at (1580, 890), clickable=false
android_logcat -> IllegalStateException in onMeasure
...reads the layout, edits it, rebuilds, taps the button, confirmsInstall
Requires Node 18+, the Android SDK platform-tools, and a JDK (Android Studio's bundled one is found automatically).
Not on npm yet — install from source:
git clone https://github.com/jjs03111/android-build-mcp
cd android-build-mcp
npm install && npm run buildClaude Code
claude mcp add android -- node /absolute/path/to/android-build-mcp/dist/index.jsAny MCP client — add to the client's server config:
{
"mcpServers": {
"android": {
"command": "node",
"args": ["/absolute/path/to/android-build-mcp/dist/index.js"]
}
}
}No configuration needed if you have a standard Android Studio install. Run android_doctor to confirm.
Related MCP server: Android MCP Server
What this does beyond wrapping adb
It knows why builds fail. JDK/Gradle incompatibility is checked before the build starts, so you get "JDK 23 is newer than Gradle 8.7 supports (max 22)" instead of Unsupported class file major version 67 after a two-minute wait. SDK location not found, signature mismatches on install, and INSTALL_FAILED codes all come back with the actual remedy attached.
It doesn't flood the context. A failed Gradle build is thousands of lines, nearly all of it task progress. The failure summary keeps the compiler diagnostics and the FAILURE: block and drops the rest — typically a 50%+ reduction, all of it noise. Logcat filters to your app's pid. android_dump_ui returns text and coordinates instead of a megabyte of screenshot when what you needed was the label on a button.
It recognises failures it has seen before. Every failure path — Gradle, install, crash logs — runs its output past a set of known Android failure modes whose symptoms point nowhere near their causes. When one matches, the fix arrives attached to the error, not in a document somebody has to remember to consult:
Gradle refusing to run on a too-new JDK (
Unsupported class file major version)SDK location not foundon a fresh clone, becauselocal.propertiesis gitignoredOEM battery management killing foreground services within seconds on Samsung and Xiaomi
adb shell input textsilently dropping every non-ASCII characterGridLayoutcells collapsing when weights meetwrap_contentPackage visibility filtering returning empty lists on Android 11+
Signature mismatches on install, and stale package references after an external uninstall
android_pitfalls exposes the same set for browsing and searching. An agent that hits one of these otherwise starts bisecting application code that was never the problem.
It finds your toolchain. adb via ANDROID_HOME, the platform's standard SDK locations, then PATH. The JDK via Android Studio's bundled JBR — chosen because it is version-matched to AGP, unlike whatever is on PATH. Gradle is launched through the wrapper's main class rather than gradlew.bat, which both sidesteps Node's refusal to spawn .bat files and guarantees the validated JDK is the one that runs.
Tools
Device
Tool | Purpose |
| Connected devices with Android version, screen size, density, rotation |
| Promote a USB device to wireless adb, or reconnect to a known address |
Build
Tool | Purpose |
| Gradle assemble, optionally install and launch in one call |
| Run unit or instrumented tests, reporting which ones failed |
| Install an APK |
| Start an app, optionally cold |
| Wipe an app's data without uninstalling it |
| Remove an app |
Inspect
Tool | Purpose |
| PNG of the screen, inline or to a file |
| View hierarchy as text, with tap coordinates for every element |
| Logs filtered by package, tag, priority, or the crash buffer |
| Arbitrary |
Interact
Tool | Purpose |
| Tap or long-press |
| Swipe, scroll, drag |
| Type into the focused field |
| Back, Home, Enter, arrows, volume |
| Force an orientation, or restore auto-rotate |
Diagnose
Tool | Purpose |
| Check adb, SDK, JDK, devices, and JDK/Gradle compatibility |
| Look up known failure modes by error text, topic, or keyword |
How this compares
vs. letting an agent run gradlew and adb through a shell tool. It already can, and when a build succeeds the difference is small. The difference shows up on failure and at scale: a failed build here returns the compiler errors instead of several thousand lines of task progress, a JDK/Gradle mismatch is caught before the build rather than two minutes into it, logs come back scoped to your app's process, and android_dump_ui hands over exact tap coordinates instead of the agent estimating them off a screenshot. Shell output is also unstructured, so every result has to be re-read by the model.
vs. mobile-mcp and other device-control servers. Those cover device automation — tap, swipe, screenshot, element inspection — and mobile-mcp covers iOS too, which this does not. The overlap is real, and if driving a device is all you need, they are the more established choice. What they do not cover is the build side: compiling the project, resolving the application id out of Gradle, installing, running tests, and explaining why the toolchain refused. That is what this server is for. Testing an app you did not build? Prefer theirs. Writing the app? This closes the edit-run-read cycle.
vs. Android Studio. Not a competitor. This exists so an agent can do the parts of the loop that do not need a person watching. Keep the IDE open.
Configuration
Everything is auto-detected. Override only if you need to:
Variable | Purpose |
| Android SDK root |
| Path to the adb binary |
| JDK to build with |
Security
android_shell runs arbitrary commands on the connected device, and the build tools execute the target project's Gradle wrapper — which is code from that project. Point this at repositories you trust, the same way you would before opening one in an IDE. Nothing is sent anywhere: every tool talks only to the local adb server and the local filesystem.
android_clear_data and android_uninstall destroy app data irrecoverably; both are marked with destructiveHint so clients that gate destructive tools can prompt before running them.
Known limitations
android_input_textcannot type non-ASCII characters, nor the literal sequence%s. Both are limitations of Android'sinput textcommand: it is ASCII-only, and it decodes%sto a space with no escape available (even%%sdecodes to%). The tool rejects both cases with an explanation rather than silently mangling the text. Use an adb-driven IME (ADBKeyBoard) for CJK.android_dump_uicannot see insideWebViewcontent; uiautomator only exposes the WebView node itself. Useandroid_screenshotfor WebView-based UIs.android_dump_uicannot see past a locked screen either — you get the keyguard, not the app.Wireless adb requires the host and the device to be on the same LAN. A VPN interface on the host does not bridge to the device's network, and routers with AP isolation block it outright.
android_connect_wifidetects the subnet mismatch case and says so rather than reporting a bare timeout.Release builds require the project's own signing config.
Developed and manually verified on Windows against a physical device (Galaxy A16, Android 16) and an emulator (API 35). macOS and Linux run the unit tests and a server smoke check in CI, and Linux additionally runs the full end-to-end suite on an emulator — but neither has been driven by hand, so rough edges in toolchain discovery are likelier there. Reports welcome.
Development
npm install
npm run build
npm test # unit tests
npm run typechecknpm test needs no device. The end-to-end suite does — start an emulator or plug in a phone, then:
node test/e2e/run.mjsIt prefers an emulator when one is running, so it will not rotate the screen or inject input on a phone you happen to have plugged in.
Contributing
The most useful contribution is a pitfall. If some Android failure cost you an afternoon because the symptom pointed nowhere near the cause, add it to src/services/pitfalls.ts: the symptom as you observed it, the actual cause, the fix, and — if the error has a recognisable signature — a regex so it fires automatically at the moment of failure instead of waiting to be looked up.
Bug reports that include the output of android_doctor are much faster to act on.
License
MIT
Available Tools
19 toolsandroid_buildBuild (and optionally install and launch) an Android appA
Run a Gradle assemble for an Android project and, when asked, install the APK and start it on a device — the full edit-run loop in one call.
Handles the toolchain details that normally break automated builds:
picks a JDK Gradle can actually run on (Android Studio's bundled JBR when present), instead of trusting the ambient JAVA_HOME
refuses to start, with an explanation, when the JDK is newer than the Gradle wrapper supports
condenses a failed build's log down to the compiler errors instead of returning thousands of progress lines
The first build of a project downloads the Gradle distribution and dependencies and can take several minutes.
Args:
project_path (string): absolute path to the Gradle root (contains gradlew)
module (string): module producing the APK (default: 'app')
variant ('debug' | 'release'): build variant (default: 'debug')
clean (boolean): run the 'clean' task first (default: false)
install (boolean): install the APK after a successful build (default: false)
launch (boolean): start the app after installing; implies install (default: false)
serial (string, optional): target device when installing
timeout_ms (number): build timeout (default: 900000)
response_format ('markdown' | 'json')
Returns: { "success": boolean, "task": string, // e.g. "assembleDebug" "durationMs": number, "apkPath": string, // present on success "packageName": string, // resolved from applicationId / namespace / manifest "gradleVersion": string, "javaMajor": number, // JDK major version used "installed": boolean, "launched": boolean, "output": string, // build tail on success, error summary on failure "hint": string // present when the failure is recognised }
Examples:
Use when: "build and run this app on my phone" -> project_path=..., install=true, launch=true
Use when: verifying a code change compiles -> project_path=..., install=false
Don't use when: the APK is already built and you only want it installed (use android_install)
Error Handling:
"Unsupported class file major version" is caught before the build starts and reported as a JDK/Gradle mismatch with the fix
"SDK location not found" means local.properties or ANDROID_HOME is missing
Install failures report the adb failure code (e.g. INSTALL_FAILED_UPDATE_INCOMPATIBLE) with the usual remedy
Inputs: project_path (required), clean, force, launch, +6 more.
android_clear_dataClear an app's dataADestructiveIdempotent
Wipe an app's data and cache while leaving it installed — the equivalent of "Clear storage" in system settings.
Use this to get back to a first-run state without uninstalling. It is less disruptive than android_uninstall (the app and its install stay put) but it still destroys everything the app has stored: databases, preferences, cached files, and any signed-in session.
Args:
package_name (string, optional): application id to clear
project_path (string, optional): Gradle root; the package is resolved from it when package_name is omitted
module (string): module to read the package from (default: 'app')
serial (string, optional): target device
Returns: { "cleared": boolean, "packageName": string, "serial": string }
Examples:
Use when: testing onboarding or a first-run migration repeatedly
Use when: a corrupt local database is masking the bug you are chasing
Don't use when: you need the app gone entirely (use android_uninstall)
Error Handling:
Reports when the package is not installed
The app is force-stopped as a side effect; relaunch it with android_launch
Inputs: module, serial, package_name, project_path.
android_connect_wifiConnect to a device over Wi-FiAIdempotent
Switch a USB-connected device to wireless adb, or reconnect to one that was paired before.
Wireless adb is what makes it practical to keep iterating on a phone that is not tethered to the machine. The device and the host must be on the same network.
Two modes:
No 'host' given — requires a USB device. Reads the device's Wi-Fi address, runs 'adb tcpip', then connects. After this succeeds the USB cable can be unplugged.
'host' given — reconnects directly to a known address, no USB needed.
Args:
host (string, optional): device IP, with or without port, e.g. "192.168.0.12" or "192.168.0.12:5555"
port (number): TCP port to use (default: 5555)
serial (string, optional): USB serial to promote to wireless, when several devices are attached
Returns: { "serial": string, "host": string, "port": number, "alreadyConnected": boolean }
Examples:
Use when: you want to unplug the cable and keep building to the device
Use when: the wireless connection dropped after the phone slept and you need it back
Don't use when: you only need to know what is connected (use android_list_devices)
Error Handling:
"no usable IPv4 address" means the device is not on Wi-Fi; connect it to the same network as this machine
A device that has rebooted loses TCP/IP mode entirely and must be re-promoted over USB
Inputs: host, port, serial.
android_doctorDiagnose the Android toolchainARead-onlyIdempotent
Check that the Android toolchain is usable and report exactly what is wrong when it is not.
Verifies adb, the SDK, the JDK, and connected devices; with a project path it also checks that project's Gradle wrapper against the JDK that would be used to build it — the mismatch that produces 'Unsupported class file major version'.
Run this first when a build or device command fails for a reason that is not obviously in the app's own code.
Args:
project_path (string, optional): Gradle root to include in the checks
module (string): module to inspect (default: 'app')
response_format ('markdown' | 'json')
Returns: { "healthy": boolean, "checks": [ { "name": string, "status": "ok"|"warn"|"fail", "detail": string, "fix": string } ], "toolchain": { "adb": string, "sdkRoot": string, "javaHome": string, "javaMajor": number } }
Examples:
Use when: a build fails and you do not yet know whether the cause is the project or the environment
Use when: adb commands fail and you want to know if the device is authorized
Use when: setting up on a new machine and want to confirm everything resolves
Error Handling:
Reports missing adb as a failed check with installation guidance, rather than throwing
Inputs: module, project_path, response_format.
android_dump_uiRead the on-screen UI treeARead-only
Dump the current screen's view hierarchy as text, with tap coordinates for every element.
This is the cheap way for an agent to see what is on screen. It gives exact text, resource ids and content descriptions, plus a centre point for each node that can be passed straight to android_tap — no guessing at pixel positions from a screenshot.
By default only meaningful nodes are returned (anything with text, a content description, a resource id, or that is clickable). Layout containers are dropped.
Args:
serial (string, optional): target device
include_all (boolean): return every node including empty containers (default: false)
filter (string, optional): case-insensitive substring; keeps only nodes whose text, id or description matches
response_format ('markdown' | 'json')
Returns: { "count": number, "nodes": [ { "index": number, "class": string, // e.g. "android.widget.Button" "text": string, "desc": string, // content-description "id": string, // resource-id "clickable": boolean, "center": [number, number], // pass to android_tap "bounds": string // "[left,top][right,bottom]" } ] }
Examples:
Use when: you need to press a button and must know where it is -> filter="submit"
Use when: verifying a screen shows the expected text after a change
Use when: a screenshot is ambiguous and you want the literal string values
Don't use when: you need to see rendering, colour or layout quality (use android_screenshot)
Error Handling:
"could not get idle state" means the UI is still animating; wait briefly and retry
WebView content is often opaque to uiautomator; a screenshot may be the only option there
Inputs: filter, serial, include_all, response_format.
android_input_textType textA
Type text into the focused input field.
Two limitations come from Android's 'input text' command itself, not from this tool. Both are rejected up front with an explanation rather than silently mangling the text:
ASCII only. Korean, Japanese, Chinese, emoji and accented letters cannot be injected at all. Use an IME that accepts adb broadcasts (ADBKeyBoard is the usual one), or set the value directly in the app under test.
The literal sequence '%s' cannot be typed. Spaces are transmitted as '%s', and Android's decoder has no escape for a real one — even '%%s' decodes to '% '.
Tap the field first (android_tap) so it has focus.
Args:
text (string): ASCII text to type
serial (string, optional): target device
submit (boolean): press Enter afterwards (default: false)
Returns: { "typed": string, "submitted": boolean, "serial": string }
Examples:
Use when: filling a login form -> text="user@example.com"
Use when: entering a search term and running it -> text="pizza", submit=true
Don't use when: the text contains non-ASCII characters (see the limitation above)
Error Handling:
Rejects non-ASCII input, and text containing a literal '%s', with an explanation instead of mangling it
Text going nowhere means no field has focus; tap the field first
Text appended to existing content means the field was not empty; clear it first
Inputs: text (required), serial, submit.
android_installInstall an APKAIdempotent
Install an APK onto a device, replacing any existing copy.
Args:
apk_path (string, optional): absolute path to the APK. Omit to use the last build output of project_path.
project_path (string, optional): Gradle root, used to locate the APK when apk_path is omitted
module (string): module that produced the APK (default: 'app')
variant ('debug' | 'release'): which build output to install (default: 'debug')
serial (string, optional): target device
grant_permissions (boolean): grant all runtime permissions at install time (default: false)
Returns: { "installed": boolean, "apkPath": string, "serial": string }
Examples:
Use when: an APK was built earlier and you want it on the device -> project_path=...
Use when: installing a downloaded APK -> apk_path=...
Don't use when: you also need to compile first (use android_build with install=true)
Error Handling:
INSTALL_FAILED_UPDATE_INCOMPATIBLE: the installed copy was signed with a different key; uninstall it first
INSTALL_FAILED_VERSION_DOWNGRADE: the device has a newer versionCode; uninstall or bump the version
INSTALL_FAILED_INSUFFICIENT_STORAGE: free space on the device
Inputs: module, serial, variant, apk_path, +2 more.
android_key_eventPress a hardware or system keyA
Send a key event — Back, Home, Enter, arrows, volume and so on.
Args:
key (string): key name without the KEYCODE_ prefix. One of: BACK, HOME, MENU, APP_SWITCH, ENTER, TAB, DEL, FORWARD_DEL, ESCAPE, DPAD_UP, DPAD_DOWN, DPAD_LEFT, DPAD_RIGHT, DPAD_CENTER, VOLUME_UP, VOLUME_DOWN, WAKEUP, SLEEP, POWER, CAMERA, SEARCH, MEDIA_PLAY_PAUSE, MEDIA_NEXT, MEDIA_PREVIOUS, PAGE_UP, PAGE_DOWN, MOVE_HOME, MOVE_END
serial (string, optional): target device
repeat (number): how many times to send it, 1-20 (default: 1)
Returns: { "key": string, "repeat": number, "serial": string }
Examples:
Use when: navigating back out of a screen -> key="BACK"
Use when: dismissing to the launcher before a cold start -> key="HOME"
Use when: submitting a form without tapping -> key="ENTER"
Use when: the screen is off and you need to see it -> key="WAKEUP" (POWER toggles, so it can turn the screen back off)
Don't use when: typing characters (use android_input_text)
Error Handling:
An unrecognised key name is rejected with the list of supported keys
Inputs: key (required), repeat, serial.
android_launchLaunch an appAIdempotent
Start an installed app's launcher activity, optionally force-stopping it first.
Args:
package_name (string, optional): application id, e.g. "com.example.myapp"
project_path (string, optional): Gradle root; the package is resolved from it when package_name is omitted
module (string): module to read the package from (default: 'app')
serial (string, optional): target device
force_stop (boolean): kill the app before starting, guaranteeing a cold start (default: false)
Returns: { "launched": boolean, "packageName": string, "serial": string }
Examples:
Use when: bringing an app to the foreground before taking a screenshot
Use when: you need a cold start to reproduce a launch-time bug -> force_stop=true
Don't use when: the app is not installed yet (use android_install)
Error Handling:
Reports when the package is not installed, rather than silently doing nothing
An app with no launcher activity cannot be started this way
Inputs: module, serial, force_stop, package_name, +1 more.
android_list_devicesList Android devicesARead-onlyIdempotent
List every Android device and emulator currently visible to adb, with model, Android version and screen geometry.
Call this first in any session: other tools need a serial when more than one device is attached, and this is where devices in a bad state (unauthorized, offline) surface.
Args:
detailed (boolean): also query each device for Android version, SDK level, screen size, density and rotation. Costs one extra adb round trip per device (default: true)
response_format ('markdown' | 'json'): output format (default: 'markdown')
Returns: { "count": number, "devices": [ { "serial": string, // e.g. "R3CN70XXXXX" or "192.168.0.12:5555" "state": string, // "device" = ready; "unauthorized" / "offline" = not usable "wireless": boolean, // connected over TCP/IP rather than USB "model": string, "androidVersion": string, // when detailed=true, e.g. "14" "sdkLevel": number, // when detailed=true, e.g. 34 "screenSize": string, // when detailed=true, e.g. "1080x2340" "density": string, // when detailed=true, e.g. "420" "rotation": number // when detailed=true, degrees: 0/90/180/270 } ] }
Examples:
Use when: starting work and you need a serial for the other tools
Use when: an install failed and you want to check the device is still authorized
Don't use when: you already have a serial and just want app logs (use android_logcat)
Error Handling:
Returns an empty device list rather than an error when nothing is plugged in
Devices in state 'unauthorized' need the on-screen "Allow USB debugging?" prompt accepted
Inputs: detailed, response_format.
android_logcatRead device logsARead-only
Read logcat from the device, filtered down to what is actually relevant.
Returns a snapshot of the existing buffer and exits; it does not stream. Filter by package to see only your app's output — this is usually what you want after a crash, because the unfiltered buffer is mostly unrelated system noise.
Args:
serial (string, optional): target device
package_name (string, optional): show only lines from this app's process
tag (string, optional): show only this log tag
priority ('V'|'D'|'I'|'W'|'E'|'F'): minimum level (default: 'V', or 'E' when crashes_only is set)
crashes_only (boolean): read the crash buffer — fatal exceptions and ANRs. This buffer survives the process, so it works after the app has died (default: false)
lines (number): how many lines to return, 1-2000 (default: 200). Normally the most recent N; with crashes_only it is the first N of the crash, since that is where the exception and the top frames are
clear_only (boolean): wipe the buffer and return immediately without reading (default: false)
response_format ('markdown' | 'json')
Returns: { "lines": string[], "count": number, "truncated": boolean, "serial": string, "pid": number, "diagnosis": string }
'diagnosis' is present when a crash matches a known Android failure mode.
Examples:
Use when: the app crashed and you need the stack trace -> crashes_only=true, package_name="com.example.app"
Use when: watching your own Log.d output -> tag="MyTag"
Use when: reproducing a bug cleanly -> clear_only=true, reproduce it, then call again to read
Don't use when: you want to see the screen (use android_screenshot)
Error Handling:
Filtering by package uses the process id, which requires the app to be running. After a crash there is no process, so combine package_name with crashes_only — that path filters the crash buffer by name instead
Inputs: tag, lines, serial, priority, +4 more.
android_pitfallsLook up known Android failure modesARead-onlyIdempotent
Search a curated set of Android development failure modes whose symptoms point nowhere near their causes.
These are problems where the obvious interpretation is wrong: a build that fails on a JDK version rather than the code, a service killed by vendor power management rather than a bug, keystrokes ignored because they are synthetic, a GridLayout that collapses because of how weights resolve. Each entry gives the symptom, the actual cause, and the fix.
Consult this when something fails in a way that does not make sense, before spending time bisecting the app's own code.
Args:
error_text (string, optional): observed error output; returns entries whose known signatures match it
topic ('build'|'device'|'service'|'input'|'layout'|'packaging', optional): filter by area
search (string, optional): case-insensitive substring match across title, symptom, cause and fix
response_format ('markdown' | 'json')
Returns: { "count": number, "pitfalls": [ { "id": string, "topic": string, "title": string, "symptom": string, "cause": string, "fix": string } ] }
Examples:
Use when: a build failed with unfamiliar output -> error_text=""
Use when: a background service keeps dying -> topic="service"
Use when: planning tablet support and you want the known traps first -> topic="layout"
Don't use when: the error is plainly in the app's own code
Error Handling:
Returns an empty list when nothing matches; that means the problem is not a known environment trap
Inputs: topic, search, error_text, response_format.
android_screenshotTake a screenshotARead-only
Capture the device screen as a PNG.
By default the image is returned inline so it can be looked at directly. Screenshots are large — a phone screen is typically 0.5–2 MB, and a tablet more — so when you only need the file (for a report, or to diff later), pass output_path and set include_image=false to keep it out of the conversation.
To read UI text or find something to tap, android_dump_ui is far cheaper than a screenshot and gives exact coordinates.
Args:
serial (string, optional): target device
output_path (string, optional): absolute path to also write the PNG to
include_image (boolean): return the image inline (default: true)
Returns: Inline image plus: { "bytes": number, "outputPath": string, "serial": string }
Examples:
Use when: confirming a layout change actually rendered as intended
Use when: the app crashed and you want to see what is on screen
Don't use when: you need the text of a view or somewhere to tap (use android_dump_ui)
Error Handling:
A black or empty image usually means a secure window (FLAG_SECURE) or a locked screen
Inputs: serial, output_path, include_image.
android_set_rotationRotate the screenAIdempotent
Force the display into a specific orientation, or hand control back to the accelerometer.
Setting a fixed rotation turns auto-rotate off first; otherwise the sensor immediately overrides it. Use 'auto' to restore normal behaviour.
Rotating is the fastest way to check a layout in both orientations. Note that an app locking its own orientation in the manifest wins over this — the display will not turn.
Args:
orientation ('portrait' | 'landscape' | 'portrait_reverse' | 'landscape_reverse' | 'auto')
serial (string, optional): target device
Returns: { "requested": string, "rotationDegrees": number, "serial": string }
Examples:
Use when: checking a tablet layout in landscape -> orientation="landscape"
Use when: restoring the device after testing -> orientation="auto"
Don't use when: the app declares a fixed screenOrientation; change the manifest instead
Error Handling:
If the reported rotation does not change, the foreground app is locking its orientation
Inputs: orientation (required), serial.
android_shellRun an adb shell commandADestructive
Run an arbitrary command on the device via adb shell, for anything the dedicated tools do not cover.
Arguments are passed as an array and are not interpreted by a host shell, so quoting is not a concern. Note that the device's shell still applies its own semantics to things like redirection.
Prefer a dedicated tool when one exists — they parse output and explain failures. Reach for this for one-off inspection: dumpsys, pm, settings, getprop and similar.
Args:
command (string[]): command and arguments, e.g. ["dumpsys", "battery"]
serial (string, optional): target device
timeout_ms (number): command timeout (default: 30000)
Returns: { "stdout": string, "command": string, "serial": string }
Examples:
Use when: reading battery state -> command=["dumpsys", "battery"]
Use when: listing installed packages -> command=["pm", "list", "packages", "-3"]
Use when: checking a system setting -> command=["settings", "get", "global", "window_animation_scale"]
Don't use when: a dedicated tool covers it (android_logcat, android_dump_ui, android_tap)
Error Handling:
The device shell's own error text is returned verbatim
Commands needing root fail on production builds; there is no workaround on a locked device
Inputs: command (required), serial, timeout_ms.
android_swipeSwipe or scrollA
Swipe between two points. Used for scrolling, dismissing, and drag gestures.
To scroll a list down (revealing content further down), swipe from a lower y to a higher one — the finger moves up.
Args:
x1, y1 (number): start point in pixels
x2, y2 (number): end point in pixels
duration_ms (number): gesture duration; longer is a drag, shorter is a fling (default: 300)
serial (string, optional): target device
Returns: { "from": [number, number], "to": [number, number], "durationMs": number, "serial": string }
Examples:
Use when: scrolling a list down -> x1=540, y1=1600, x2=540, y2=600
Use when: dragging an item -> duration_ms=1000
Don't use when: a single tap is enough (use android_tap)
Error Handling:
A swipe that is too fast to register is the usual cause of "nothing happened"; raise duration_ms
Inputs: x1 (required), x2 (required), y1 (required), y2 (required), +2 more.
android_tapTap the screenA
Tap at a screen coordinate.
Get coordinates from android_dump_ui, which returns a ready-to-use centre point for every element. Coordinates are in device pixels and depend on the current rotation, so re-read the UI after rotating.
Args:
x (number): horizontal position in pixels
y (number): vertical position in pixels
serial (string, optional): target device
long_press (boolean): hold instead of tapping (default: false)
duration_ms (number): hold duration when long_press is set (default: 600)
Returns: { "tapped": [number, number], "longPress": boolean, "serial": string }
Examples:
Use when: android_dump_ui reported a button at (540, 1683) -> x=540, y=1683
Use when: opening a context menu -> long_press=true
Don't use when: you do not know where the element is (call android_dump_ui first)
Error Handling:
Coordinates outside the screen are silently ignored by Android; verify with android_dump_ui afterwards
Inputs: x (required), y (required), serial, long_press, +1 more.
android_testRun a project's testsA
Run a Gradle project's tests and report which ones failed.
This is the verification half of the loop: after changing code, prove the change works rather than assuming it. Failure output is condensed the same way android_build condenses it — you get the failing test names, not the whole Gradle log.
Two kinds of test:
'unit' runs testDebugUnitTest on the JVM. Fast, no device needed.
'instrumented' runs connectedDebugAndroidTest on a connected device or emulator. Slower, and requires a device.
Args:
project_path (string): absolute path to the Gradle root (contains gradlew)
module (string): module whose tests to run (default: 'app')
kind ('unit' | 'instrumented'): which test task to run (default: 'unit')
tests (string, optional): Gradle --tests filter, e.g. "com.example.MyTest" or ".LoginTest."
timeout_ms (number): timeout (default: 900000)
force (boolean): skip the JDK/Gradle compatibility pre-check (default: false)
response_format ('markdown' | 'json')
Returns: { "success": boolean, "task": string, // e.g. ":app:testDebugUnitTest" "durationMs": number, "failures": string[], // failing test names, when parseable "output": string, // condensed failure summary, or the tail on success "hint": string // present when the failure is a known one }
Examples:
Use when: you changed logic and want to know it still passes -> kind="unit"
Use when: narrowing to one failing test -> tests="com.example.CalcTest"
Use when: the behaviour only shows on a device -> kind="instrumented"
Don't use when: you only need the app compiled (use android_build)
Error Handling:
"instrumented" with no device connected fails at the device check, before Gradle starts
A project with no test sources reports success with an up-to-date task rather than a failure
Inputs: project_path (required), kind, force, tests, +3 more.
android_uninstallUninstall an appADestructiveIdempotent
Remove an app from the device. This deletes the app's data and cannot be undone.
Args:
package_name (string, optional): application id to remove
project_path (string, optional): Gradle root; the package is resolved from it when package_name is omitted
module (string): module to read the package from (default: 'app')
serial (string, optional): target device
keep_data (boolean): keep app data and cache directories (adb uninstall -k) (default: false)
Returns: { "uninstalled": boolean, "packageName": string, "serial": string }
Examples:
Use when: an install fails with INSTALL_FAILED_UPDATE_INCOMPATIBLE and the old copy must go
Use when: testing a first-run experience from a clean state
Don't use when: you only want to clear state; force-stopping or clearing data is less destructive
Error Handling:
Reports when the package was not installed to begin with
Inputs: module, serial, keep_data, package_name, +1 more.
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
Loading tool schema changelog…
TDQS
Scored across 19 tools
Each tool has a clearly distinct purpose: device enumeration, Wi-Fi connection, build, test, app lifecycle (install/launch/clear/uninstall), UI interaction (screenshot/dump/tap/swipe/input/key/rotation), and diagnostics (logcat/shell/doctor). Even overlapping actions like android_build with install=true are explicitly separated from android_install and android_launch, so an agent can reliably pick the right tool.
All tools share the 'android_' prefix and snake_case, but the second part mixes styles: some are verb_noun (list_devices, clear_data, set_rotation), some are single verbs (build, test, install, tap), and some are nouns (pitfalls, logcat, shell, doctor). This is readable and predictable in terms of prefix, but the verb/noun pattern is not consistent across the set.
With 19 tools, the count is slightly above the ideal 3–15 range, but each tool covers a distinct aspect of Android development (device management, build/test, app lifecycle, UI automation, diagnostics). The number feels appropriate for the broad scope, and none are redundant, so it earns a slightly-over-but-reasonable rating.
The tool surface covers the full lifecycle: build, test, install, launch, uninstall, clear data; UI interaction (tap, swipe, input, key events, rotation, screenshot, UI dump); device discovery and Wi-Fi connection; logcat with crash analysis; arbitrary shell access; and a toolchain doctor. There are no obvious dead ends, and the shell tool acts as an escape hatch for anything not explicitly covered.
Related MCP Connectors
Email inboxes, webhook URLs, mock APIs and 170+ tools for coding agents over one MCP connection.
MCP server for building and testing AI agents with multi-model experimentation and insights.
Remote MCP for Android CLI agent build gate, structured receipts, audit logs, and reviewer-ready evi
MCP server for your apps' tools and custom tools, plus hosted AI agents and approval-gated workflows
Related MCP Servers
- AlicenseBqualityBmaintenanceA comprehensive MCP server that enables AI agents to interact with Android devices through Android Debug Bridge (ADB), offering 198 tools for device control, app management, diagnostics, and more.209175 npm23Apache 2.0
- FlicenseAqualityDmaintenanceAn MCP server designed for Android development, enabling AI assistants to directly control Android devices for screenshots, UI analysis, app management, and more.19-
- AlicenseBqualityAmaintenanceA local MCP server that controls Android devices via ADB, enabling AI agents and users to manage devices, execute UI automation, and perform testing through 27 MCP tools and an optional web console.271MIT
- FlicenseNot gradedqualityAmaintenanceMCP server that provides AI coding agents real Android development tools—Gradle, adb, logcat, lint, crash triage—through a local, permissioned interface. Enables agents to inspect projects, run safe Gradle tasks, capture logs/screenshots, and triage crashes.1-