mobile-mcp
Allows control and automation of an Android phone over Wi-Fi ADB, exposing mobile-mcp tools to interact with the device, including tapping, typing, installing/uninstalling apps, opening URLs or app deep links, and other device actions.
This server does not currently have a deployable release on Glama. Follow the repository's setup instructions to install and configure it for your MCP client, including any required credentials and local dependencies.
mobile-mcp-image
Container image for mobile-next/mobile-mcp, which has no published image,
bundled with adb so it can drive an Android phone over Wi-Fi adb from a Kubernetes Pod or any container host.
Unmodified upstream:
@mobilenext/mobile-mcpfrom npm, every dependency pinned bypackage-lock.json(version + sha512). This repo only adds an entrypoint.adb: Google Android SDK Platform-Tools, pinned by sha256.
Runtime:
gcr.io/distroless/nodejs22-debian13— no shell, no package manager; runs as uid 1000.Image:
ghcr.io/mouse9527/mobile-mcp-image:sha-<short commit>(linux/amd64; built only frommain; nolatest).
Component | Version |
mobile-mcp | 1.0.12 (mobilecli 1.0.21) |
platform-tools (adb) | 37.0.1 |
Node.js | 22 (distroless, Debian 13) |
What the entrypoint does
Refuses to start unless
MOBILEMCP_AUTHis set (mobile-mcp otherwise serves without authentication), orMOBILEMCP_AUTH_DISABLED=1explicitly allows that (for use behind an authenticating proxy). Setting both is an error.Copies
$ADB_KEY_DIR/adbkeyto~/.android/adbkeywith mode 0600, so the phone's pairing survives restarts. adb derives the public key from the private key; noadbkey.pubis needed.Starts the adb server with mDNS off (
ADB_MDNS=0; container networks usually drop multicast).If
PHONE_ADDRis set, checks everyKEEPALIVE_SECONDSwhether the phone is connected and runsadb connecton each ofPHONE_ADB_PORTSuntil one works. It logs one line only when the state changes.Starts mobile-mcp's Streamable HTTP server on
MCP_LISTEN(/mcp), in the same process.
Telemetry (PostHog, scarf.sh) is off by default (MOBILEMCP_DISABLE_TELEMETRY=1).
Related MCP server: phone-mcp-server
Environment
Variable | Default | Meaning |
| — (required unless disabled) | Bearer token clients must send |
| unset |
|
| empty | Phone host name or IP. Empty = do not connect (the MCP server still starts) |
|
| Ports to try, space separated. |
|
| Reconnect check interval (minimum 5) |
| empty | Directory containing |
|
|
|
|
| Must be writable (adb key copy, mobilecli daemon socket) |
| unset | mobile-mcp: set |
|
| mobile-mcp telemetry switch |
| unset | mobile-mcp: also append each tool call, with timestamps, to this file |
Writable paths needed with a read-only root filesystem: $HOME (default /tmp/home) — mount an emptyDir on /tmp.
Run
# generate a key once and keep it; pairing is bound to it
openssl genrsa -traditional -out adbkey 2048
docker run --rm -p 8931:8931 \
-e MOBILEMCP_AUTH="$(openssl rand -hex 32)" \
-e PHONE_ADDR=<phone host or IP> \
-e ADB_KEY_DIR=/keys -v "$PWD/adbkey:/keys/adbkey:ro" \
ghcr.io/mouse9527/mobile-mcp-image:sha-<short commit>Pair the phone once from inside the container (Android 11+: Developer options → Wireless debugging → Pair device with pairing code):
docker exec -it <container> adb pair <phone>:<pairing port> # enter the 6-digit code
docker exec -it <container> adb connect <phone>:<debug port>
docker exec -it <container> adb -s <phone>:<debug port> tcpip 5555 # switch to a fixed portAfter a phone reboot tcpip 5555 is lost: re-enable Wireless debugging and run the last two commands again with the new port.
Lease gateway (optional, PHONE_LEASE=1)
When several agents share one phone through an MCP gateway that opens a new upstream session for every tool call
(LiteLLM does), per-session locking is useless and multi-step tasks interleave (agent A reads the screen, agent B
opens another app, agent A taps). With PHONE_LEASE=1 the entrypoint puts a small gateway (gateway.js, same
process, no dependencies) on MCP_LISTEN and moves mobile-mcp to LEASE_INNER_LISTEN (default 127.0.0.1:8932).
mobile-mcp itself is unchanged; with the variable unset nothing changes.
One holder at a time. A holder's tool call takes the phone if it is free and nobody is queued; every call extends the lease to now +
LEASE_IDLE_S. Others wait FIFO inside their call for up toLEASE_WAIT_S, then get a tool error (isError) saying the phone is busy and their queue position (keptLEASE_TICKET_S, while someone holds the phone). When the lease ends, the first caller still waiting in a call gets it, reservedLEASE_GRACE_Sfor its next call; queued callers that are not waiting any more are skipped, so the phone is never parked for someone who left. State is in memory: a restart frees the phone.Who is the holder (first match):
x-phone-holderheader (set by the client's MCP config) >x-openwebui-chat-id(Open WebUI) > the client's MCP session id at the gateway in front, forwarded asmcp-session-id(mobile-mcp is stateless, so it is on every request) > nothing (call-level exclusion only, never keeps the phone). A client session seen for the first time only getsLEASE_SESSION_FIRST_IDLE_S, so callers that open a new session per call do not wait behind themselves. LiteLLM: listmcp-session-id,x-openwebui-chat-id,x-phone-holderin the server'sextra_headers.Extra tools in
tools/list:lease_status,lease_hold(seconds, keep the phone while waiting for the user, capped atLEASE_HOLD_MAX_S),lease_release.LEASE_TOKENS=1also addslease_acquireand an optionalleaseargument on every tool, for callers that cannot be identified otherwise (off by default: it changes tool schemas).Logs: one JSON line per event on stderr (
"src":"lease"): time, event, a sha256 prefix of the holder, how it was identified, tool name, wait time. Never arguments, results or raw holder ids.
Variable | Default |
|
|
|
|
|
|
|
|
|
|
|
|
| unset (off) |
|
|
Screen helpers (optional, with the lease gateway)
PHONE_WAKE=1: before a tool that needs the screen, the gateway checksdumpsys power(at most everyWAKE_CHECK_S, default 15 s, also refreshed by every successful call). If the screen is notAwake:KEYCODE_WAKEUP(never POWER, which toggles),wm dismiss-keyguard(for phones without a lock-screen password), waitWAKE_SETTLE_MS(800), check again; still off → a clear tool error, mobile-mcp is not called. A read-only call (elements, foreground app, screenshot) that still finds no UI hierarchy gets one recovery round (screen on: VOLUME_UP + VOLUME_DOWN, which ends MIUI's pocket mode; screen off: WAKEUP again) and one retry, then a clear error. Uses the adb server of the container.ELEMENTS_TRIM=1: shortermobile_list_elements_on_screentext (also insidemobile_batch_commands): identical overlapping elements listed once, zero-size ones dropped, resource ids without the package prefix, a label equal to the text written once, centre pointc=x,yinstead of the top-left corner. No visible string is dropped and refs keep their original numbers (taps by ref re-read the screen).format=jsonis not touched.Both off unless set; with both off tool calls are passed through unchanged. Log events:
screen_woken,screen_recover,screen_still_no_ui,screen_check_failed.
Security notes
Anyone holding the Bearer token can fully control the phone (tap, type, install / uninstall apps, open any URL scheme). Keep the endpoint on a private network and restrict who can reach it.
The adb server listens only inside the container; do not publish adb ports.
Upgrading
mobile-mcp: change the version in
package.json, then regenerate the lockfile from the public npm registry:npm install --package-lock-only --ignore-scripts --omit=dev --registry https://registry.npmjs.org. Check the upstream changelog for new tools and update the expected tool count insmoke.sh.platform-tools: change
PLATFORM_TOOLS_VERSIONandPLATFORM_TOOLS_SHA256in the Dockerfile. Google's repository manifest (https://dl.google.com/android/repository/repository2-3.xml) lists sha1 only; download the zip, check its sha1 against the manifest, then record its sha256.Base images: update the digests in
NODE_IMAGE/RUNTIME_IMAGE; keep both on the same Debian release.
CI
.github/workflows/image.yml: build linux/amd64 → audit.sh (no adb keys, credentials or .git in the image;
file list and hashes uploaded as an artifact) → smoke.sh (refuses to start without a token, 401 without token,
33 tools, adb ≥ 37, uid 1000, no shell, telemetry off; lease gateway with PHONE_LEASE=1: queueing, release, FIFO,
hold, holder identity, first-call probation — smoke/lease_test.py) → push to ghcr from main only.
License
MIT for the files in this repository (LICENSE). The image also contains third-party software under its own
licenses; see NOTICE.md.
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
Loading tool schema changelog…
This server cannot be deployed
Maintenance
Related MCP Connectors
Give your AI agent a real cloud Android phone: push APKs, drive it, get screenshots and logs.
Cloud Android phones for AI agents: create a phone, read the screen, tap, type, install APKs, park.
Let AI agents control Android and iOS devices on Mobilerun Cloud: screen, input, apps, workflows.
Disposable cloud Android emulators for coding agents: run an APK or PR build, tap, type, screenshot.
Related MCP Servers
- AlicenseAqualityDmaintenanceEnables AI models to control Android devices via ADB through natural language commands, supporting screen analysis and automated actions.346 npmMIT
- AlicenseNot gradedqualityBmaintenanceEnables AI agents to control Android phones via MCP and HTTP. Supports screen capture, taps, swipes, text input, and app management.7AGPL 3.0
- FlicenseBqualityBmaintenanceEnables an AI agent to control an Android phone over a private Tailscale network via accessibility UI automation, screenshots, gestures, notifications, clipboard, intents, SAF file access, and optional Termux/Shizuku shells without root or ADB.65-
- AlicenseNot gradedqualityCmaintenanceEnables AI agents to control a physical Android device or Docker-hosted emulator over ADB by taking screenshots and performing taps, swipes, key presses, text input, shell commands, app launches, and device info queries. It runs as a local MCP stdio server alongside a web UI and REST API for remote access.1MIT