Skip to main content
Glama
mouse9527
by mouse9527

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-mcp from npm, every dependency pinned by package-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 from main; no latest).

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

  1. Refuses to start unless MOBILEMCP_AUTH is set (mobile-mcp otherwise serves without authentication), or MOBILEMCP_AUTH_DISABLED=1 explicitly allows that (for use behind an authenticating proxy). Setting both is an error.

  2. Copies $ADB_KEY_DIR/adbkey to ~/.android/adbkey with mode 0600, so the phone's pairing survives restarts. adb derives the public key from the private key; no adbkey.pub is needed.

  3. Starts the adb server with mDNS off (ADB_MDNS=0; container networks usually drop multicast).

  4. If PHONE_ADDR is set, checks every KEEPALIVE_SECONDS whether the phone is connected and runs adb connect on each of PHONE_ADB_PORTS until one works. It logs one line only when the state changes.

  5. 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

MOBILEMCP_AUTH

— (required unless disabled)

Bearer token clients must send

MOBILEMCP_AUTH_DISABLED

unset

1 = serve without authentication. Only behind something that enforces access (authenticating proxy + network policy)

PHONE_ADDR

empty

Phone host name or IP. Empty = do not connect (the MCP server still starts)

PHONE_ADB_PORTS

5555

Ports to try, space separated. 5555 after adb tcpip 5555; Android 11+ wireless debugging uses a random port

KEEPALIVE_SECONDS

20

Reconnect check interval (minimum 5)

ADB_KEY_DIR

empty

Directory containing adbkey (RSA private key, PEM). Mount a secret here

MCP_LISTEN

0.0.0.0:8931

[host:]port for the MCP endpoint

HOME

/tmp/home

Must be writable (adb key copy, mobilecli daemon socket)

MOBILEMCP_ALLOW_UNSAFE_URLS

unset

mobile-mcp: set 1 to let mobile_open_url open app deep links (non-http schemes)

MOBILEMCP_DISABLE_TELEMETRY

1

mobile-mcp telemetry switch

LOG_FILE

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 port

After 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 to LEASE_WAIT_S, then get a tool error (isError) saying the phone is busy and their queue position (kept LEASE_TICKET_S, while someone holds the phone). When the lease ends, the first caller still waiting in a call gets it, reserved LEASE_GRACE_S for 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-holder header (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 as mcp-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 gets LEASE_SESSION_FIRST_IDLE_S, so callers that open a new session per call do not wait behind themselves. LiteLLM: list mcp-session-id, x-openwebui-chat-id, x-phone-holder in the server's extra_headers.

  • Extra tools in tools/list: lease_status, lease_hold (seconds, keep the phone while waiting for the user, capped at LEASE_HOLD_MAX_S), lease_release. LEASE_TOKENS=1 also adds lease_acquire and an optional lease argument 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

LEASE_IDLE_S

120

LEASE_SESSION_FIRST_IDLE_S

30

LEASE_WAIT_S

45

LEASE_TICKET_S

90

LEASE_GRACE_S

30

LEASE_HOLD_MAX_S

600

LEASE_TOKENS

unset (off)

LEASE_INNER_LISTEN

127.0.0.1:8932

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 in smoke.sh.

  • platform-tools: change PLATFORM_TOOLS_VERSION and PLATFORM_TOOLS_SHA256 in 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.

Related MCP Connectors

Related MCP Servers

  • F
    license
    B
    quality
    A
    maintenance
    Enables 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
    -
  • A
    license
    Not graded
    quality
    B
    maintenance
    Enables 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.
    1
    MIT