Give your coding agent eyes on the actual Android device it's building for — screenshot, tap, swipe, read the UI tree, install and launch apps — the same way Antigravity's browser tool gives it control of a browser.
I was building my second Android app — a kana learning app, in Kotlin — and skipped installing full Android Studio for it. Just a command-line toolchain: Gradle, adb, USB debugging, and Antigravity as the coding agent.
That worked fine for writing code. It broke down the moment something needed checking. The agent could write a UI change, but it had no way to actually see the app running — no screenshot, no way to tap through it, nothing. Every time something looked wrong, I was the one launching the app, describing what I saw in English, and hoping the description was precise enough for the agent to find the bug. For anything visual — spacing, alignment, sizing — that back-and-forth was slower than just fixing it myself.
Antigravity already had a browser tool that could screenshot, click, and scroll a real browser for web work. There was no reason the same idea couldn't work for a physical Android device over adb. So with Claude's help, I built this: an MCP server that gives the agent the same kind of hands-on access to a phone that the browser tool gives it to a browser.
It worked well enough that the agent could fix a UI bug entirely on its own — build, install, screenshot, compare, adjust, repeat — without me describing anything. See below.
I asked the agent to fix uneven padding on a toggle component, gave it access to this tool, and told it not to stop until a screenshot actually confirmed the fix.
| ❌ Before | ✅ After |
|---|---|
![]() |
![]() |
No back-and-forth. The agent made the change, ran the debug build, installed it, took a screenshot through this tool, compared it against what I'd asked for, and only reported back once the screenshot actually matched.
| Tool | What it does |
|---|---|
📋 list_devices |
See what's attached (USB or Wi-Fi) |
📶 enable_wireless_debugging / connect_network / pair_network / disconnect_network |
Attach a device over Wi-Fi |
📦 install_apk / launch_app / stop_app / clear_app_data / list_packages |
App lifecycle |
🖼️ screenshot |
See the current screen, as an image |
🌳 get_ui_hierarchy |
Read every element's text, id, and tap coordinates — the "DOM" equivalent |
👆 find_and_tap |
Tap an element by visible text or resource-id, no coordinates needed |
✋ tap / swipe / input_text / press_key |
Drive the UI directly |
📜 get_logcat |
Pull recent logs, e.g. a crash stack trace |
🚪 adb_shell |
Escape hatch for anything else — off by default, see Security |
Everything runs through adb, so it works against a real phone or any adb-visible emulator — nothing here is specific to my app.
It's a plain MCP server, run over stdio, spawned as a local subprocess by whatever agent host you're using (Antigravity, Claude Code, anything that speaks MCP). Every tool call shells out to adb and returns the result — screenshots come back as real image content the agent can see, not just a file path.
agent <-- MCP (stdio) --> android-mcp <-- adb --> phone
No cloud, no account, nothing installed on the device beyond what adb already needs. Everything stays on your machine.
- Android platform-tools (
adbon PATH — runadb versionto check) - Python 3.10+
- On the phone: Developer options unlocked, USB debugging on (Settings > About phone > tap Build number 7x, then Settings > Developer options)
pip install -r android_mcp/requirements.txtUSB: plug in, accept the debugging prompt, adb devices should list it.
Wireless, two ways:
- Bootstrap over USB once: call
enable_wireless_debugging(), thenconnect_network("<ip>:5555")with the address it returns. You can unplug USB after connecting. - Fully cable-free (Android 11+): Settings > Developer options > Wireless debugging > Pair device with pairing code, then
pair_network(address, code)followed byconnect_network()with the main address shown on that screen.
Copy mcp_config.example.json, fill in the absolute path to android_mcp/android_mcp.py on your machine, and drop it into your MCP config location (for Antigravity: .agents/mcp_config.json in your project, or ~/.gemini/config/mcp_config.json globally):
{
"mcpServers": {
"android-control": {
"command": "python",
"args": ["/absolute/path/to/android_mcp/android_mcp.py"]
}
}
}💡 Tip: If your setup also needs environment activation (a separate SDK/toolchain script, a virtualenv, etc.), point
commandat a wrapper script instead — seeandroid_mcp/run_android_mcp.bat.examplefor the pattern. Chaining multiple activation commands directly inside the JSON tends to break on quoting; a small wrapper script that does the activation and then runspython android_mcp.pyis far more reliable.
Something like, in your AGENTS.md or equivalent:
You have android-control tools to run and check the app you're building,
the same way you'd use a browser tool for a web app. After any UI change:
build, install_apk, launch_app, then screenshot to confirm it actually
looks right before saying you're done. get_ui_hierarchy gives exact tap
coordinates and lets you read on-screen text you can't trust OCR for.
npx @modelcontextprotocol/inspector python android_mcp/android_mcp.pyThat opens a web UI listing every tool with a form to call it and see the raw result — screenshot, tap, read the UI tree, all without an agent in the loop. Confirm the basics work, confirm adb_shell correctly refuses until you explicitly enable it, then wire it in.
This gives an LLM shell-level access to a device. That's real risk — a hallucinated command could delete photos, clear messages, or worse. A few things are built in to reduce that:
- 🧯 Run this against an emulator or a spare device with nothing irreplaceable on it. This is the actual fix. Android Studio's AVD Manager gives you a free emulator that adb treats identically to a real phone. Everything below reduces risk if you use your daily driver anyway — it doesn't eliminate it.
- 🔐
adb_shellis off by default. It's the one unrestricted tool, so it only turns on if you setANDROID_MCP_ALLOW_SHELL=1in the server's own environment — outside the conversation, where the agent can't touch it. - 🚫 Even when enabled, destructive patterns are hard-blocked in code:
rm -rf,mkfs,dd if=, format/wipe/factory-reset, raw SMS/contacts/call-log access,su. - 🛡️
stop_app/clear_app_datarefuse a fixed list of system and comms packages — Contacts, Messaging, Phone, Play Services, SystemUI, the launcher — regardless of anything else. - 🛡️ Every value that reaches
adb shellisshell-quoted(shlex.quote) before it's sent, includingpackage names,filter strings, andlog tags— adb shell reconstructs its arguments into a single string and runs it through a remote shell on the device, so an unquoted value containing shell metacharacters could be reinterpreted there instead of treated as a literal string.
None of that makes the shell tool safe to point at data you can't lose. It makes an accidental or malformed call much less likely, which is a different guarantee than "safe" — testing it yourself a number of times with no incident is evidence the common paths work, not proof of an edge case that hasn't come up yet, especially once other people are running this against their own devices and agents.
- 🔤
input_textis ASCII-only at the adb layer — for verifying non-Latin text (kana, etc.), read it back viaget_ui_hierarchyinstead of typing it in. - 📐
get_ui_hierarchyreports(0,0)for elements without proper accessibility bounds — common with some custom-rendered UI (e.g. certain Compose components missing semantics). That's an app-side accessibility gap, not something this tool can work around — and worth fixing on the app side anyway, since real screen-reader users hit the same gap. - 🖥️ Tested on Windows + a real device; should work anywhere
adbdoes, but I haven't tried it on Linux/macOS or against an emulator myself yet.
See CONTRIBUTING.md.
I built this because I needed it, not to start a project. If it's useful to you, use it. If you improve it, I'd genuinely appreciate a mention — that's really the only ask here.
MIT — see LICENSE.

