Skip to content

acunningham-ship-it/android-agent

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

2 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

android-agent — a phone for agents

An agent-drivable Android device, exposed to Claude as native MCP tools. The mobile sibling of veil (stealth browser): instead of a browser or an NLE, the agent gets a real Android 13 screen it can see (screenshot), read (a numbered element list), and act on (tap / swipe / type / launch / install).

A headless redroid container is the device. ADB is the wire. A small FastMCP stdio server (droid) is the tool surface Claude Code talks to.

Claude Code ──stdio(MCP)──► server.py (droid) ──adb──► redroid (Android 13)
                                                        127.0.0.1:5555

Quickstart

./droidctl.sh up          # reload binder, start the container, wait for boot
# once "BOOTED (localhost:5555)" prints, the droid tools are live in Claude Code

The MCP server is already registered at user scope (claude mcp get droid✔ Connected), so the 11 tools are available in every Claude Code session on this box. Ask Claude to "screenshot the phone" or "open Settings and turn on airplane mode" and it drives the device.

Standalone smoke test (acts as an MCP client against the live device):

./.venv/bin/python smoke_mcp.py

Tools

Tool What it does
droid_status Device health: connection, boot state, screen size, foreground app. Call first if anything looks off.
droid_screenshot Current screen as PNG. How the agent sees the phone. Retries once on an empty capture.
droid_ui On-screen elements as a numbered list with tap coords: #i [Class] "label" clickable @(x,y). The accessibility-tree trick — grounds taps without pixel-guessing.
droid_tap Tap (x, y). Coords come from droid_ui.
droid_swipe Swipe (x1,y1)→(x2,y2) over a duration. Scrolling/dragging.
droid_type Type text into the focused field (submit=True presses Enter; settle_ms waits before typing).
droid_key Press a nav/hardware key: BACK, HOME, RECENTS, ENTER, DEL, MENU, POWER, VOLUME_UP/DOWN, or a raw keycode.
droid_launch Launch an app by package or name substring. Resolves the launcher activity and am starts it.
droid_list_apps List installed packages (third_party_only=False for system apps).
droid_install Install/reinstall a local .apk.
droid_shell Arbitrary adb shell command. Escape hatch / power tool.

Typical loop: droid_screenshot + droid_ui to see and locate → droid_tap / droid_type to act → screenshot again to confirm.

Architecture

  • redroid (Android 13) — a full Android userspace in a --privileged Docker container. Software-rendered (redroid_gpu_mode=guest), 720x1280, no video/GPU.
  • binderfs — Android needs the binder IPC drivers. On this kernel (6.8) they come from the binder_linux module surfaced via a binderfs mount at /dev/binderfs/{binder,hwbinder,vndbinder}not the older devices= module param. droidctl.sh up handles modprobe + mount idempotently.
  • ADB — bound to 127.0.0.1:5555 only (see Security). Every tool call goes through _adb()adb -s localhost:5555 …, with an idempotent _ensure() reconnect so a dropped connection self-heals.
  • FastMCP stdio server (server.py, name droid) — 11 @mcp.tool() functions. Launched by run-mcp.sh, which Claude Code's MCP registration runs.

Lifecycle (droidctl.sh)

./droidctl.sh up               # ensure binder + start/create container + wait boot
./droidctl.sh down             # stop the container (userdata persists in ./data)
./droidctl.sh restart          # docker restart + reconnect adb
./droidctl.sh rm               # remove the container (keeps ./data volume)
./droidctl.sh status           # container + boot_completed + screen size
./droidctl.sh shell <cmd>      # adb shell passthrough
./droidctl.sh logs             # container logs (tail)
./droidctl.sh screenshot <f>   # dump a PNG to a file

Persistence across reboots

The container's restart policy is no, and this is deliberate: redroid needs /dev/binderfs mounted before it starts, so an auto-restart on boot would race the mount and come up with broken binder IPC. After a host reboot, bring it back with the one command that does the ordering correctly:

./droidctl.sh up

up reloads the binder_linux module, mounts binderfs, starts the existing container (userdata in ./data survives), and blocks until sys.boot_completed=1. Verified: ./droidctl.sh down && ./droidctl.sh up cleanly cycles the device and ADB reconnects.

(Optional, not installed: a --restart unless-stopped policy or a user systemd unit would auto-start it, but only if you also guarantee binderfs is mounted first — not worth the fragility here. droidctl.sh up is the supported path.)

Known limitations (honest)

  • tap → type pacing. Right after a screen transition the target field may not have focus yet, so the first characters of droid_type can drop. Tap the field, let it settle, then type — or pass droid_type(..., settle_ms=300). Typing into an already-focused field is reliable (spaces preserved via %s encoding).
  • droid_type is plain-text only. Shell metacharacters (& ; | < > ( ) " ' …) are passed through adb shell input text unescaped and can misbehave. Fine for words, queries, URLs; don't feed it arbitrary payloads.
  • monkey is unreliable on redroid (it frequently no-ops), so droid_launch resolves the launcher activity and uses am start -n instead, falling back to monkey only when no launcher activity exists.
  • ADB is localhost-only by design. Bound to 127.0.0.1:5555; there is no auth on ADB, so it must never be exposed off-box. Reach it over an SSH/tailnet tunnel if remote access is ever needed.
  • No GPU / no video. Software rendering only (redroid_gpu_mode=guest). Games and video playback will be slow or black; this is a UI-automation device, not a media device.
  • Privileged container. redroid requires --privileged for binder/ashmem. Run it only on a trusted host (it is, here).

Files

  • server.py — the FastMCP droid server (11 tools).
  • droidctl.sh — container + device lifecycle.
  • run-mcp.sh — launch wrapper Claude Code's MCP registration invokes.
  • smoke_mcp.py — end-to-end MCP client test against the live device.
  • .venv/ — has mcp installed (recreate: python3 -m venv .venv && .venv/bin/pip install mcp).
  • data/ — redroid userdata volume (gitignored).

About

A phone for AI agents: drive a headless Android device over ADB as MCP tools (screenshot, tap, type, launch, UI element-tree). Built on redroid + FastMCP.

Topics

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages