Skip to main content
Glama

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

Cursor — 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 --selftest

Requirements

Piece

Needed for

Android SDK platform-tools (adb)

Everything. Found via ANDROID_HOME, else the usual per-OS SDK location.

A device or emulator with USB debugging

Everything.

Android SDK emulator

Only android_emulator_start. Resolved by path, not PATH.

Gradle wrapper in your project

Only android_build / reinstall=True.

ffmpeg

Only video analysis.

LM Studio with a vision (VLM) model

Only android_assert, android_record_stop, android_analyze and analyze=.

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

android_build(project_dir, tasks, install, release_locks)

Lock-safe Gradle build. On failure returns a full diagnostic report plus a brief path. tasks=[":app:compileDebugKotlin"] gives a fast compile-only check.

android_install(project_dir | apk, allow_uninstall)

adb install -r -d -t, with INSTALL_FAILED_* codes explained.

android_session_start(package, project_dir, reinstall, fresh)

The entry point: optionally build+install, launch, and return screenshot + UI dump. A crash on launch returns the mapped stack trace instead.

android_prepare(package)

Grants what pm grant cannot: usage-stats and overlay appops, notifications, optionally an accessibility service.

Look and interact

Tool

What it does

android_screenshot(scale, analyze)

Screenshot as an image block; analyze="..." adds a local-VLM answer.

android_ui_dump(compact)

Ref-addressable elements: text, content-desc, class, centre, bounds, flags.

android_tap(ref | text | x,y)

Tap by ref, visible text, or coordinates.

android_swipe(direction | x1,y1,x2,y2)

Scroll or gesture.

android_type(text, ref, submit)

Type into a field (ASCII only — an adb limitation).

android_key(key)

BACK, HOME, ENTER, APP_SWITCH, …

android_wait(ms | until_text | until_gone)

Wait for a condition instead of screenshotting mid-animation.

android_devices() / android_emulator_start(avd)

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

android_crashes(package, project_dir, only_new)

Java exceptions, native signals and ANRs, with app frames mapped to source and a fixer brief.

android_logcat(package, grep)

Recent logcat scoped to the app's pid.

android_gradle_daemons(project_dir, stop)

Every Gradle/Kotlin daemon with its user home, JDK and IDLE/BUSY state, and which hold your build outputs. stop="lockers" | "foreign" | "all-idle" stops idle ones only.

android_vlm_log(last, call, full)

Which local-model calls were made, by which tool, and what the model saw and said.

Local vision model

Tool

What it does

android_assert(question)

Screenshot → local VLM → PASS/FAIL + evidence. Costs the driving model almost no context.

android_record_start / android_record_stop(analyze, fps)

Record the screen, then have the local model narrate what happened.

android_analyze(question, images, video)

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:

  1. 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's gradleJvm, else Studio's bundled JBR). A sandboxed GRADLE_USER_HOME in the environment is ignored. Sequential builds from Studio and the agent then share one daemon, and a process cannot lock files against itself.

  2. Never starting a second daemon while one in that pool is BUSY; it waits (wait_if_busy_s).

  3. Checking locks before starting, and refusing with the holder named when something outside Studio's pool holds the outputs.

  4. 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(), or android-agent --cli vlm-log [n | v3].

Configuration

Variable

Default

Meaning

ANDROID_AGENT_DEVICE

first physical device

Pin the default target serial.

ANDROID_AGENT_GRADLE_USER_HOME

persistent GRADLE_USER_HOME, else ~/.gradle

The Gradle user home Android Studio uses.

ANDROID_STUDIO_JDK

Studio's bundled JBR

JDK for projects without a pinned daemon JVM.

ANDROID_AGENT_GRADLE_TIMEOUT

1800

Build time limit, seconds.

ANDROID_AGENT_SCALE

1280

Long edge of screenshots returned to the model.

ANDROID_AGENT_VLM_SCALE

768

Long edge of images sent to the local model.

ANDROID_AGENT_LMSTUDIO_URL

http://127.0.0.1:1234

LM Studio endpoint.

ANDROID_AGENT_LMSTUDIO_MODEL

gemma-4-26b-a4b-it

Model id for analysis; set this to whatever vision model you run.

ANDROID_AGENT_LMSTUDIO_TIMEOUT

300

Seconds; a cold model load is slow.

ANDROID_AGENT_OUT

~/.android-agent

Where screenshots, videos, logs, briefs and the VLM log go.

ANDROID_HOME

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 devices

The 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 @tool decorator converts everything, which is what makes failures readable.

  • Screenshots must be binary-clean: adb exec-out screencap -p with a list argv, never a shell pipe.

  • Stopping a recording takes exactly one SIGINT; a second signal truncates the mp4 before the moov atom is written.

  • Compose exposes no resource-ids — match on text/content-desc, or fall back to coordinates.

License

MIT

Available Tools

21 tools
android_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

    A4.1/5.0

    Scored across 21 tools

    Disambiguation4/5

    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.

    Naming Consistency5/5

    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.

    Tool Count4/5

    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.

    Completeness4/5

    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

    Related MCP Servers