android-agent
Provides tools for building an Android app, installing and launching it on a connected device or emulator, driving the UI via screenshots, taps, swipes, and text input, and diagnosing crashes, ANRs, and build failures.
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-agent build and launch my app, then tap the login button and type test@example.com"
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-agent
An MCP server that lets a coding agent (Claude Code, Cursor, or anything else that speaks MCP) build an Android app, drive it on a real device, and debug it — instead of editing Kotlin blind and asking you what the screen shows.
Screenshots come back as real image blocks, so the model looks at the pixels directly.
Video goes through a local vision model (LM Studio), so a 60-second recording returns as a paragraph instead of 200 frames of context.
UI dumps are ref-addressable (
n1,n2, …), so taps hit real controls rather than guessed coordinates.Builds don't fight Android Studio. They run in Studio's own Gradle daemon pool, wait while Studio is building, and name the process holding a locked file. No more "R.jar: The process cannot access the file because it is being used by another process".
Failures come back complete: compile errors with source context, crashes and ANRs with stack frames mapped to your files, install errors explained — plus a self-contained brief you can hand to a fixer subagent.
Scope is live UI driving plus build/install. It does not run Espresso/JUnit suites.
Install
Needs uv and the Android SDK platform-tools (adb).
Claude Code:
claude mcp add android-agent --scope user -- uvx --from git+https://github.com/Fdondi/android-agent-mcp android-agentCursor — in ~/.cursor/mcp.json (or .cursor/mcp.json in a project):
{
"mcpServers": {
"android-agent": {
"command": "uvx",
"args": ["--from", "git+https://github.com/Fdondi/android-agent-mcp", "android-agent"]
}
}
}Restart the host (or reload its MCP servers) afterwards. To pin a version, append @v0.1.0 to the
git URL.
Check the install, which verifies binaries, devices, the crash log, Gradle daemons and the local model in one go:
uvx --from git+https://github.com/Fdondi/android-agent-mcp android-agent --selftestRequirements
Piece | Needed for |
Android SDK platform-tools ( | Everything. Found via |
A device or emulator with USB debugging | Everything. |
Android SDK emulator | Only |
Gradle wrapper in your project | Only |
ffmpeg | Only video analysis. |
LM Studio with a vision (VLM) model | Only |
Platform support
Developed and tested on Windows 11. The device, build, crash and vision features are plain
adb/Gradle and are portable. The Gradle daemon inventory and file-lock detection are
OS-specific: Windows uses the Restart Manager API and Get-CimInstance; macOS and Linux use
/proc or lsof plus ps. The POSIX paths are implemented but have not been exercised on a
real macOS or Linux machine — bug reports welcome.
Related MCP server: Android Builder MCP Server
Tools
Build, install, run
Tool | What it does |
| Lock-safe Gradle build. On failure returns a full diagnostic report plus a brief path. |
|
|
| The entry point: optionally build+install, launch, and return screenshot + UI dump. A crash on launch returns the mapped stack trace instead. |
| Grants what |
Look and interact
Tool | What it does |
| Screenshot as an image block; |
| Ref-addressable elements: text, content-desc, class, centre, bounds, flags. |
| Tap by ref, visible text, or coordinates. |
| Scroll or gesture. |
| Type into a field (ASCII only — an adb limitation). |
|
|
| Wait for a condition instead of screenshotting mid-animation. |
| List devices and AVDs; boot an AVD and wait for it. |
Every interaction result checks the device's crash log, so a crash is never silent: results carry
!! APP CRASHED or !! APP NOT RESPONDING.
Diagnose
Tool | What it does |
| Java exceptions, native signals and ANRs, with app frames mapped to source and a fixer brief. |
| Recent logcat scoped to the app's pid. |
| Every Gradle/Kotlin daemon with its user home, JDK and IDLE/BUSY state, and which hold your build outputs. |
| Which local-model calls were made, by which tool, and what the model saw and said. |
Local vision model
Tool | What it does |
| Screenshot → local VLM → |
| Record the screen, then have the local model narrate what happened. |
| Ask the local model about media already on disk. |
A typical loop
android_session_start(package="com.example.app", project_dir="/path/to/project", reinstall=True)
-> builds, installs, launches, returns a screenshot + addressable elements
android_tap(text="Sign in")
android_type(text="demo@example.com", ref="n7", submit=True)
android_wait(until_text="Welcome")
android_assert("is the welcome screen visible with a Continue button?")
android_crashes(only_new=True)For a whole flow rather than one screen:
android_record_start(max_seconds=45)
... taps and swipes ...
android_record_stop(handle="rec1", analyze="did any screen flash an error or render blank?")Building alongside Android Studio
On Windows a Gradle daemon keeps build outputs such as R.jar open for a while after a build. A
build in a different daemon then cannot rewrite them, which is the classic
"being used by another process" failure. Agent terminals make this routine: some run Gradle under
their own sandboxed GRADLE_USER_HOME, and a daemon there can never be reused by Android Studio.
android_build avoids it by:
Building in Studio's pool — its Gradle user home (your persistent
GRADLE_USER_HOME, else~/.gradle) and its Gradle JDK (the project's daemon JVM criteria, else.idea/gradle.xml'sgradleJvm, else Studio's bundled JBR). A sandboxedGRADLE_USER_HOMEin the environment is ignored. Sequential builds from Studio and the agent then share one daemon, and a process cannot lock files against itself.Never starting a second daemon while one in that pool is BUSY; it waits (
wait_if_busy_s).Checking locks before starting, and refusing with the holder named when something outside Studio's pool holds the outputs.
release_locks=True, which stops idle Gradle/Kotlin daemons holding the outputs. Busy builds and the IDE are never touched; an idle daemon costs only warm-up time.
So: agents should never run gradlew in a terminal — use android_build.
What a failure report contains
Category and next step:
COMPILE_ERROR,RESOURCE_ERROR,FILE_LOCKED,DAEMON_BUSY,INSTALL_FAILED,DEPENDENCY_ERROR,BUILD_SCRIPT_ERROR,JDK_ERROR,SDK_ERROR,OUT_OF_MEMORY,CONFIGURATION_CACHE,TIMEOUT,UNKNOWN.Every error from Kotlin (K1 and K2), javac, KSP/kapt, AAPT, manifest merger and build scripts, with file:line:col and ±3 lines of source, each marked
[changed in working tree]or[not changed]. An error at an unchanged call site usually means a callee changed elsewhere.Gradle's own explanation, the working-tree changes, the lock holders (PID, daemon kind, Gradle user home, JDK, IDLE/BUSY), the environment, and the full log path.
Crash reports lead with the root cause (Caused by), collapse framework frames, and map app frames
to your source with surrounding lines.
Briefs and the fixer subagent
Every failure also writes a markdown brief (build-failure-*.md / crash-*.md in the run
directory) holding the task, the exact tool call that reproduces and verifies the fix, the project's
own rule files, and the full report — ready to hand to a subagent.
agents/android-fixer.md is such a subagent. Copy it to ~/.cursor/agents/ (Cursor) or
~/.claude/agents/ (Claude Code), then: "use android-fixer on <brief path>".
Was a local model used?
Only android_screenshot(analyze=...), android_assert, android_record_stop and
android_analyze ever call LM Studio. Everything else is plain adb/Gradle.
Each such result ends with a tag like
[local model gemma-4-26b-a4b-it, call v1 in run-20260922-174231, 3.0s, 1 image(s)]. No tag means no
local model was involved. Beyond that:
~/.android-agent/vlm-calls.jsonl— one JSON line per call: calling tool, model requested versus served, prompt, response, usage, duration, and any error.~/.android-agent/run-*/vlm/<id>/image-NN.png— the exact images the model received.android_vlm_log(), orandroid-agent --cli vlm-log [n | v3].
Configuration
Variable | Default | Meaning |
| first physical device | Pin the default target serial. |
| persistent | The Gradle user home Android Studio uses. |
| Studio's bundled JBR | JDK for projects without a pinned daemon JVM. |
|
| Build time limit, seconds. |
|
| Long edge of screenshots returned to the model. |
|
| Long edge of images sent to the local model. |
|
| LM Studio endpoint. |
|
| Model id for analysis; set this to whatever vision model you run. |
|
| Seconds; a cold model load is slow. |
|
| Where screenshots, videos, logs, briefs and the VLM log go. |
| auto-detected | Android SDK root. |
Device selection: explicit device= → ANDROID_AGENT_DEVICE → first physical device → first
available. A plugged-in phone wins over a running emulator.
Development
git clone https://github.com/Fdondi/android-agent-mcp
cd android-agent-mcp
uv run --script tests/test_parsers.py # offline: no device, Gradle or LM Studio
uv run --script src/android_agent/server.py --selftest
uv run --script src/android_agent/server.py --cli devicesThe server is one module, src/android_agent/server.py. It carries a PEP 723 header so it also runs
standalone with uv run --script; its dependency list must stay in sync with pyproject.toml.
Notes for anyone editing it:
stdout is the MCP wire — log to stderr only.
MCP 2.x hides exception text unless it is a
ToolError; the@tooldecorator converts everything, which is what makes failures readable.Screenshots must be binary-clean:
adb exec-out screencap -pwith a list argv, never a shell pipe.Stopping a recording takes exactly one SIGINT; a second signal truncates the mp4 before the
moovatom is written.Compose exposes no resource-ids — match on text/content-desc, or fall back to coordinates.
License
MIT
Available Tools
21 toolsandroid_analyzeA
Ask the local LM Studio VLM about images or a video already on disk.
Args: question: What to ask about the media. images: Paths to image files. video: Path to a video file, sampled into frames. fps: Frames per second when sampling a video. max_frames: Cap on frames sent. model: LM Studio model id; defaults to the configured one.
Inputs: question (required), fps, model, video, +2 more.
android_assertA
Check a claim about the current screen using the local VLM. Returns PASS or FAIL.
A cheap oracle: the screenshot goes to LM Studio, not into your context.
Args: question: A yes/no claim, e.g. "is the timed-apps list visible?" device: Device serial; defaults to the first physical device. model: LM Studio model id; defaults to the configured one.
Inputs: question (required), model, device.
android_buildA
Build the project with Gradle, safely alongside Android Studio, and explain any failure.
ALWAYS use this instead of running gradlew in a terminal: an agent terminal runs Gradle in a sandboxed daemon that keeps R.jar locked against Android Studio. This tool runs the build in Android Studio's own daemon pool, waits if Studio is building, and names any process holding a locked file.
On failure the result is a complete report: category, every compiler/resource error with its file, line and surrounding source, Gradle's explanation, recently changed files, and the path of a self-contained markdown brief you can hand to a fixer subagent.
Args: project_dir: Gradle root (the folder containing settings.gradle[.kts] and gradlew). module: App module, used to derive the default task and find the APK. variant: Build variant, e.g. debug or freeDebug. tasks: Explicit Gradle tasks. Default "::assemble". For a fast compile-only check use [":app:compileDebugKotlin"]. install: Install the built APK on the device afterwards. device: Device serial for install; defaults to the first physical device. release_locks: Stop IDLE Gradle/Kotlin daemons that hold this project's build outputs (e.g. one left behind by an agent terminal). Never touches busy builds. allow_uninstall: If install hits a signature/version conflict, uninstall first (wipes data). wait_if_busy_s: How long to wait for a busy Android Studio daemon before giving up. timeout_s: Build time limit.
Inputs: project_dir (required), tasks, device, module, +6 more.
android_crashesA
Show recent crashes and ANRs (Java exceptions, native signals, not-responding) with stack traces.
App stack frames are mapped to your source files with the surrounding lines, and a self-contained markdown brief is written for a fixer subagent.
Args: package: Only this app's crashes. Defaults to the package of the current session. project_dir: Gradle root, to map stack frames to source. Defaults to the session's. device: Device serial; defaults to the first physical device. only_new: Only crashes since the last check (interaction tools check automatically). max_crashes: How many of the most recent crashes to show.
Inputs: device, package, only_new, max_crashes, +1 more.
android_devicesA
List attached Android devices and bootable emulator AVDs.
Shows which device the other tools will use by default (first physical device, or ANDROID_AGENT_DEVICE if set). Call this first when unsure what is connected.
Inputs: no named parameters.
android_emulator_startA
Boot an emulator AVD and wait until it is ready.
Args: avd: AVD name as listed by android_devices. wait: Block until sys.boot_completed is set. timeout: Max seconds to wait for boot.
Inputs: avd (required), wait, timeout.
android_gradle_daemonsA
List Gradle and Kotlin daemons, show which ones lock a project's build outputs, optionally stop some.
Explains the classic "R.jar is being used by another process": an idle daemon from a different Gradle user home (e.g. an agent terminal's sandbox) keeps build outputs open.
Args: project_dir: Gradle root whose build outputs to check for locks. stop: None (just list), "lockers" (IDLE daemons holding this project's outputs), "foreign" (IDLE daemons outside Android Studio's Gradle user home - Studio can never reuse them), or "all-idle". Busy daemons and the IDE are never stopped.
Inputs: stop, project_dir.
android_installA
Install an already-built APK (no Gradle involved).
Args: project_dir: Gradle root; the APK is found from the module's build outputs. apk: Explicit APK path instead of project_dir. module: App module inside project_dir. variant: Build variant whose APK to install. device: Device serial; defaults to the first physical device. allow_uninstall: On signature/version conflicts, uninstall first (WIPES APP DATA). grant_permissions: Grant all runtime permissions at install time (-g).
Inputs: apk, device, module, variant, +3 more.
android_keyA
Press a hardware/system key.
Args: key: BACK, HOME, ENTER, TAB, DEL, APP_SWITCH, MENU, POWER, VOLUME_UP, ... The KEYCODE_ prefix is added automatically, and a raw keycode number also works. device: Device serial; defaults to the first physical device. screenshot: Return a screenshot of the result. scale: Long-edge px for that screenshot.
Inputs: key (required), scale, device, screenshot.
android_logcatA
Read recent logcat, scoped to one app's process by default.
For crashes and ANRs prefer android_crashes, which also maps stack frames to source.
Args: package: Scope to this app's pid. Omit for all output. device: Device serial; defaults to the first physical device. lines: How many trailing lines to return. grep: Case-insensitive substring filter applied after retrieval. clear_first: Clear the buffer instead of reading (returns immediately).
Inputs: grep, lines, device, package, +1 more.
android_prepareA
Grant the special permissions that pm grant cannot, so an app is actually testable.
Sets usage-stats and overlay appops, grants POST_NOTIFICATIONS, and optionally enables an accessibility service. Safe to call repeatedly.
Args: package: App package id, e.g. com.example.app device: Device serial; defaults to the first physical device. accessibility_service: Fully-qualified service, e.g. com.x/.service.MyService
Inputs: package (required), device, accessibility_service.
android_record_startA
Start recording the screen. Interact normally, then call android_record_stop.
Args: device: Device serial; defaults to the first physical device. max_seconds: Hard stop (device caps this at 180s). size: Optional WxH, e.g. "720x1560", to shrink the capture.
Inputs: size, device, max_seconds.
android_record_stopA
Stop a recording, then have the local VLM describe what happened.
The video is sampled into frames and sent to LM Studio, so a 60s recording comes back as a paragraph instead of flooding your context with images.
Args: handle: The id returned by android_record_start. analyze: Question for the VLM. Defaults to a general description of the interaction. fps: Frames sampled per second of video. max_frames: Cap on frames sent to the model. model: LM Studio model id; defaults to the configured one.
Inputs: handle (required), fps, model, analyze, +1 more.
android_screenshotA
Capture the screen and return it as an image you can look at directly.
Args: device: Device serial; defaults to the first physical device. scale: Long-edge px for the returned image (full-res is saved to disk regardless). analyze: Optional question - also runs the shot through the local LM Studio VLM and attaches its text answer.
Inputs: scale, device, analyze.
android_session_startA
Start a driving session: optionally build+install, launch the app, and return the first look.
This is the main entry point. Returns a screenshot (as an image you can see directly) plus the UI dump. If the build fails you get the full build report; if the app crashes on launch you get the stack trace mapped to source instead of a screenshot of the launcher.
Args:
package: App package id, e.g. com.example.app
activity: Launch activity; resolved automatically if omitted.
device: Device serial; defaults to the first physical device.
project_dir: Gradle project root. Needed for reinstall, and lets crash stack traces be
mapped to source files.
reinstall: Build with android_build and install the result first.
fresh: pm clear the app first, wiping its data for a clean run.
module, variant: Which APK to build/install.
release_locks, allow_uninstall: Passed through to the build/install (see android_build).
scale: Long-edge px for the returned screenshot.
Inputs: package (required), fresh, scale, device, +7 more.
android_swipeA
Swipe or scroll, either by direction or by explicit coordinates.
Args: direction: One of up, down, left, right - a scroll gesture sized to the screen. "down" scrolls content downward (finger moves up). x1, y1, x2, y2: Explicit start/end points, used when direction is omitted. duration_ms: Gesture duration; longer is gentler. device: Device serial; defaults to the first physical device. screenshot: Return a screenshot of the result. scale: Long-edge px for that screenshot.
Inputs: x1, x2, y1, y2, +5 more.
android_tapA
Tap the screen, by ref from a dump, by visible text, or by raw coordinates.
Args: ref: Ref from android_ui_dump, e.g. "n7". Re-resolved if the dump went stale. text: Visible text or content-desc to match instead of a ref. x, y: Raw coordinates (use when the hierarchy exposes nothing tappable). device: Device serial; defaults to the first physical device. screenshot: Return a screenshot of the result. scale: Long-edge px for that screenshot.
Inputs: x, y, ref, text, +3 more.
android_typeA
Type text into the focused field, optionally tapping a field first.
ASCII only - adb's input text cannot send unicode.
Args: text: The text to type. ref: Optional ref to tap first, to focus that field. device: Device serial; defaults to the first physical device. submit: Press ENTER afterwards. screenshot: Return a screenshot of the result. scale: Long-edge px for that screenshot.
Inputs: text (required), ref, scale, device, +2 more.
android_ui_dumpA
Dump the on-screen accessibility hierarchy as a table of tappable refs.
Each row gives a ref (n1, n2, ...) usable with android_tap, plus text, content-desc, class, centre point and bounds. Prefer this over guessing pixel coordinates.
Note: Jetpack Compose apps usually expose no resource-ids - match on text/content-desc. If a screen returns no nodes, fall back to android_screenshot and tap by x/y.
Args: device: Device serial; defaults to the first physical device. compact: Show only interactive/labelled nodes (recommended).
Inputs: device, compact.
android_vlm_logA
Show which local-model (LM Studio) calls were made, by which tool, and what the model saw and said.
Every call from any host (Cursor, Claude Code) or run is recorded in ~/.android-agent/vlm-calls.jsonl, and the exact images sent to the model are saved next to it. Only android_screenshot(analyze=...), android_assert, android_record_stop and android_analyze use the local model - nothing else does.
Args: last: How many of the most recent calls to show. call: Show one call in full, by id ("v3") or "run/id" ("run-20260922-173012/v3"). full: Show full prompts and answers instead of the first lines.
Inputs: call, full, last.
android_waitA
Wait for a fixed delay, or until some text appears or disappears.
Use this instead of screenshotting into the middle of an animation.
Args: ms: Fixed sleep in milliseconds. until_text: Poll until this text/content-desc is present. until_gone: Poll until this text/content-desc is absent. timeout_ms: Give up after this long when polling. device: Device serial; defaults to the first physical device.
Inputs: ms, device, timeout_ms, until_gone, +1 more.
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
Loading tool schema changelog…
TDQS
Scored across 21 tools
Most tools target a distinct action or resource (tap vs. swipe, type vs. key vs. wait, build vs. install, crashes vs. logcat), so an agent can generally select correctly. The main ambiguity is among the VLM-related paths: android_screenshot with analyze, android_assert, and android_analyze can all answer visual questions, though their descriptions make the boundaries clear enough.
Every tool uses the consistent android_ prefix with snake_case, and most follow a predictable verb_noun pattern like android_tap, android_build, android_swipe, and android_record_stop. Even noun-based names such as android_crashes and android_logcat fit naturally with the established naming style.
21 tools is above the typical 3-15 range, but each tool addresses a genuinely distinct phase of the Android build/test/drive workflow: boot, build, install, drive, observe, record, and analyze. It feels slightly heavy rather than bloated, and each tool earns a plausible place in the toolset.
The toolset covers the full Android agent lifecycle well: build, install, launch, interact, inspect, assert, capture crashes, and read logs are all present. Minor gaps remain, such as no explicit uninstall or package-management tool, but these can be worked around via android_install's allow_uninstall and session_start's fresh flag.
Related MCP Connectors
Give your AI agent a real cloud Android phone: push APKs, drive it, get screenshots and logs.
Disposable cloud Android emulators for coding agents: run an APK or PR build, tap, type, screenshot.
Cloud Android phones for AI agents: create a phone, read the screen, tap, type, install APKs, park.
AI agents on real phones: run tasks, read the screen, tap, type and install apps.
Related MCP Servers
- AlicenseAqualityDmaintenanceEnables an agent to inspect and interact with Android emulators or physical devices via ADB, capturing UI snapshots, tapping nodes, typing text, and reading app logs.10MIT
- AlicenseNot gradedqualityDmaintenanceEnables AI agents to act as local Android developers with compilation, emulator, asset management, and diagnostics capabilities for a closed-loop build cycle.MIT
- AlicenseAqualityDmaintenanceEnables AI agents to build, deploy, drive, and debug Android apps — managing Gradle builds, emulators, adb deployment, logcat capture, and full UI automation.5376 npmMIT
- AlicenseAqualityDmaintenanceEnables AI assistants to build, drive, and observe Android/KMM apps end-to-end through ADB and Gradle, with tools for UI automation, error capture, and testing.3343 npmMIT